Repository Pattern with Masonite

#python #framework #masonite #tutorial
Written By: Joe Mancuso

Introduction

This article will walk through how to implement the Repository Pattern with a Masonite application.

The "Repository Pattern" has always been strange to me and never really made sense as to why it's called a repository. Maybe because I believe the name "repository" should be left to version control repositories so it's hard for me to link a second definition to the word repository.

For the purposes of this article and your life moving forward, let's think of a repository as "a collection of data you can use in your business logic layer".

The Problem

Maybe for a further explanation, there are 4 layers to any application:

  • Business Logic Layer (Controllers)
  • Presentation Logic Layer (Views)
  • Data Accession Layer (Models)
  • Data Storage Layer (Databases/MySQL/Postgres/etc etc)

It's typically a good idea not to mix these layers together because they usually tightly couple one implementation to another. After writing software for a while you might have realized you do this all the time by importing models into your controller logic.

So most times we use our models inside our controllers by simply importing them into the file and using them. Let's say we are building a business lead maintenance program for our current employer:

from app import Leads

class SomeController:

    def show(self):
        leads = Leads.all()

Great so this gets all the leads from the Leads model. But what if we have a few separate lead models. We might have something like this:

from app import LocalLeads, ForeignLeads

class SomeController:

    def show(self):
        local_leads = LocalLeads.all()
        foreign_leads = ForeignLeads.where('sales_id', '>', 20).get()

This might be what we need to do in several controller methods:

from app import LocalLeads, ForeignLeads, WallStreetLeads

class SomeController:

    def show(self):
        local_leads = LocalLeads.all()
        foreign_leads = ForeignLeads.where('sales_id', '>', 20).get()
        
        # More Logic Here
        ...

        return view('sales', 
            {'local': local_leads,
            'foreign': foreign_leads
            ...
            }) 

    def sales(self):
        local_leads = LocalLeads.all()
        foreign_leads = ForeignLeads.where('sales_id', '>', 20).get()
        
        # More Logic Here
        ...

        return view('sales', 
            {'local': local_leads,
            'foreign': foreign_leads
            }) 

Problem 2

The second problem with this approach is that this may work on a small scale or even with a single controller but if we need to use this model logic in multiple controllers or even in middleware and other parts of our code, we're going to run into issues there because then we may have very redundant code spread out throughout our application.

Repositories help with this by making all of the model accession logic in 1 place we can share in any parts of our code. Any changes that need to be made or made within the repository, not our controllers, middleware or other parts of our code.

Solution 1 (with issues)

One solution would be to put all that logic into the constructor if they are always the same in each method:

from app import LocalLeads, ForeignLeads

class SomeController:

    def __init__(self):
        self.local_leads = LocalLeads.all()
        self.foreign_leads = ForeignLeads.where('sales_id', '>', 20).get()

    def show(self):

        # More Logic Here
        ...

        return view('sales', 
            {'local': self.local_leads,
            'foreign': self.foreign_leads
            ...
            }) 

    def sales(self):
        
        # More Logic Here
        ...
        return view('sales', 
            {'local': self.local_leads,
            'foreign': self.foreign_leads
            ...
            }) 

Real World Scenario

But the issue is that what if they need to chance based on the controller method they are in. In a real world scenario we likely have something like this where they are usually different:

from app import LocalLeads, ForeignLeads
class SomeController:

    def show(self):
        local_leads = LocalLeads.all()
        foreign_leads = ForeignLeads.where('sales_id', '>', 20).get()
        
        # More Logic Here
        ...
        return view('sales', 
            {'local': local_leads,
            'foreign': foreign_leads
            }) 

    def sales(self):
        local_leads = LocalLeads.first()
        foreign_leads = ForeignLeads.where('order_id', 2).get()
        
        # More Logic Here
        ...

        return view('sales', 
            {'local': local_leads,
            'foreign': foreign_leads
            }) 

Now we can't really abstract away these values. Plus all of this logic is VERY redundant.

Not only is this a problem for this controller but what if we need to use the same exact logic in a different controller? Now we need to basically copy and paste all the logic into a second or even third controller sometime down the road.

Introducing Repositories

We can simply make a repository class. Using Masonite we can add it to a app/repositories directory:

app/
  http/
  repositories/
    LeadRepository.py

Let's open that repsository up and make a basic class:

class LeadRepository:

    def get_sales():
        pass

    def get_domestic_sales():
        pass

Ok great so we have a repository now that we want to get all sales and all domestic sales. Let's try to abstract what we had in our controller into here. For the purposes of this specific application we will put each one into a dictionary because that may be our requirement:

from app import LocalLeads, ForeignLeads

class LeadRepository:

    def get_sales():
        return {
            'local': LocalLeads.all(),
            'foreign': ForeignLeads.where('sales_id', '>', 20).get()
        }

    def get_domestic_only():
        return {
            'local': LocalLeads.all(),
            'foreign': ForeignLeads.where('country', 'US').get()
        }

Great now let's import this into our controller and try again:

from app.repositories import LeadRepository

class SomeController:

    def __init__(self):
        self.leads = LeadsRepository()

    def show(self):
        sales = self.leads.get_sales()

        
        # More Logic Here
        ...
    
        return view('sales', 
            {'local': sales['local'],
            'foreign': sales['foreign']
            }) 


    def sales(self):
        sales = self.leads.get_domestic_only()

        
        # More Logic Here
        ...
    
        return view('sales', 
            {'local': sales['local'],
            'foreign': sales['foreign']
            }) 

Great that's it! A Repository can be used to abstract away any dynamic model fetching that we might be using in multiple controllers or even throughout our application. We can now take that repository and add it to any controllers we need it to and all the logic remains in a single location in our code.

If you liked this article, be sure to head over to the GitHub Repo and give it a star or join the Masonite Slack Channel

Copyright Masonite 2019