DEV Community

Joseph Mancuso for Masonite

Posted on

Masonite ORM (Orator)

Introduction

Nearly every framework comes with come kind of ORM. An ORM is an Object Relational Mapper and is simply a $10 word for a $1 definition. In laymen terms, an ORM is simply a way to interact with a database in an "object" related way. In Python's case, the "object" related way is to use classes.

Typically, 1 table maps to 1 class so we can interact with our class which in turn interacts with our table.

We call these classes "Models."

Masonite uses the Orator ORM as it's ORM of choice. This ORM may be different than ORM's in the past. If you are coming from any other Python framework than this will be a bit of a new learning experience for you. If you are coming from a framework like Laravel or Ruby on Rails then this ORM will make a lot of sense right off the bat.

Before you read

Before you start reading, note that we will go into detail here on how the ORM can be used with the Masonite framework and will not be going into detail on how database migrations work. That will need to be it's own article and you can read more about that at Masonite's Database Migrations documentation page.

Once you have your database, migrations and tables setup you can learn how to interact with it using this article here.

Configuration

If you have read the migrations documentation then you likely already have the configuration setup but for those who haven't we can go into it here again.

Configuration is really simple with Masonite. All database configuration files can be found in your .env file. In this file you will find something that looks like this:

DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=masonite
DB_USERNAME=root
DB_PASSWORD=root
Enter fullscreen mode Exit fullscreen mode

Go ahead and setup any configuration settings you need.

MySQL and Postgres

The configuration settings inside the .env file are good enough for both MySQL and Postgres but you will also need drivers to support your database connection.

If you are using MySQL you have 2 options:

$ pip install PyMySQL
Enter fullscreen mode Exit fullscreen mode

or

$ pip install mysqlclient
Enter fullscreen mode Exit fullscreen mode

If you are using Postgres then you only have 1 option:

$ pip install psycopg2
Enter fullscreen mode Exit fullscreen mode

SQLite

If you are using SQLite you have a bit more to do. You don't need any drivers but you need to remove some settings from the config/database.py file.

Open up that file and there should be a section that looks like:

DATABASES = {
    'default': {
        'driver': os.environ.get('DB_DRIVER'),
        'host': os.environ.get('DB_HOST'),
        'database': os.environ.get('DB_DATABASE'),
        'user': os.environ.get('DB_USERNAME'),
        'password': os.environ.get('DB_PASSWORD'),
        'prefix': ''
    }
}
Enter fullscreen mode Exit fullscreen mode

For SQLite we need to remove the host, user and password options here and make it:

DATABASES = {
    'default': {
        'driver': os.environ.get('DB_DRIVER'),
        'database': os.environ.get('DB_DATABASE'),
        'prefix': ''
    }
}
Enter fullscreen mode Exit fullscreen mode

This is because SQLite does not need those options and will throw an exception if found.

Usage

Ok great so if you have made it this far then you know how to make migrations by reading the migrations documentation, you know how to configure MySQL or Postgres. Now we just need to know how to access our data in our database.

Getting Data

There are a few different ways we can get data from the table. For the purposes of this article we will be using the User model to generalize everything.

Getting by Key

We can get a specific row by it's primary key by using the find() method which requires the ID:

def show(self):
    User.find(1)
Enter fullscreen mode Exit fullscreen mode

This will return the record with the primary key of 1.

Getting All Records

Theres going to be a lot of times you want to get all records of a table. You can do so like this:

def show(self):
    User.all()
Enter fullscreen mode Exit fullscreen mode

This will return a collection of all the results of the table.

Getting by Value

We can also get by value of a specific column. If using raw SQL this will be analogous to a WHERE keyword. Maybe not-so-coincidentally, the method is called where:

def show(self):
    User.where('email', 'user@email.com').get()
Enter fullscreen mode Exit fullscreen mode

This will return a collection of results, even if it's a single result. More than once time per day I usually forget to add that little get() method at the end so make sure you append that method so you actually fetch the result.

Getting the first record

We can also get the first record in a result which will return an actual model instead of a collection:

def show(self):
    User.where('email', 'user@email.com').first()
Enter fullscreen mode Exit fullscreen mode

This will return the first result from the collection.

Aggregate Where

You can use what are called "aggregate methods" which is simply a way to collect a range of values.

def show(self):
    User.where('id', '<' '2').get()
Enter fullscreen mode Exit fullscreen mode

Plucking values

You can "pluck" values from a collection. In other words you can return a new collection with only the values from the column you plucked:

def show(self):
    User.where('id', '<' '2').get().pluck('email')
Enter fullscreen mode Exit fullscreen mode

This will return a new collection with ONLY the emails of these users.

Where In

You can use a this method to get values where in a list of other values. For example we can get all records who's emails are inside a list we provide:

def show(self):
    User.where_in('id', [1, 2]).get()
Enter fullscreen mode Exit fullscreen mode

This will return all rows where id is 1 or 2.

This is particularly useful while using the pluck method from above. We can pluck all id columns from another model.

We might want to call all posts that have authors that are active for example:

def show(self):
    Post.where_in(
        'id',
        User.where('active', '1').get().pluck('id')
    )
Enter fullscreen mode Exit fullscreen mode

This code simply gets all users who are active and plucks their ID which returns a collection of ID's which we can then use to pass into the where_in method to return a new result with only the posts of users who are active.

Where Not In

We can do the same thing as above but have code that shows users are are not active by using a different where_not_in method:

def show(self):
    Post.where_not_in(
        'id',
        User.where('active', '1').get().pluck('id')
    )
Enter fullscreen mode Exit fullscreen mode

This simply uses the where_not_in method instead of the where_in method.

Updating information

Once we fetch the information we have it might be useful if we can update that id. There are a few ways to do that which we will go into in this section.

Update method

We can use the update method to update a row that we fetched:

def show(self):
    User.find(1).update(email='new@email.com')
Enter fullscreen mode Exit fullscreen mode

or we can pass in a dictionary:

def show(self):
    User.find(1).update({'user-email': 'new@email.com'})
Enter fullscreen mode Exit fullscreen mode

Notice that when we pass in a dictionary, we can use column names that may not be valid Python keyword arguments. That's really the only difference between the two.

We can also update properties on the model and call the save() method:

def show(self):
    user = User.find(1)
    user.email = 'new@email.com'

    user.save()
Enter fullscreen mode Exit fullscreen mode

Updating several records

You might want to update several records at once like updating all users to an active state after a certain action:

def show(self):
    User.where('id', '>', 10).update(active=0)
Enter fullscreen mode Exit fullscreen mode

This will run the update on all records found.

Updating Timestamps

Sometimes you only want to update a records timestamps. For example you might want to fetch a user but also update the modified_at timestamp:

def show(self):
    user = User.find(1)
    user.touch()
Enter fullscreen mode Exit fullscreen mode

Creating New Records

Creating new records is also really simple. By simply creating a new model instances we can create new records.

Creating New Records With Properties

def show(self):
    user = User()
    user.name = 'Joe'
    user.email = 'name@email.com'
    user.save()
Enter fullscreen mode Exit fullscreen mode

This will create a new record.

Creating New Records With create Method

We can use the create method:

def show(self):
    user = User.create(name='Joe', email='name@email.com')
Enter fullscreen mode Exit fullscreen mode

Deleting

We can also delete records:

def show(self):
    User.find(1).delete()
    User.where('id', '>', 10).delete()
    User.destroy(1, 2, 3)
Enter fullscreen mode Exit fullscreen mode

Relationships

Relationships are extremely simple with Orator. In other Python frameworks you may have specified the foreign key relationship in a class property or class attribute. With Orator you are doing something similar but slightly different.

Has One

Let's say we have a foreign key that matches 1 user to 1 desk.

We can link this relationship inside our model like this:

from orator.orm import has_one
from app.Desk import Desk

class User(Model):

    @has_one
    def desk(self):
        return Desk
Enter fullscreen mode Exit fullscreen mode

Orator tries to guess what the foreign key's are and it is based on this logic of:

# other_table_key, local_key 
@has_one('other_id', 'table_id')
Enter fullscreen mode Exit fullscreen mode

and maps the id's to the table names. For example, this would technically be the default values and be exactly the same as the model code above:

from orator.orm import has_one
from app.Desk import Desk

class User(Model):

    @has_one('desk_id', 'user_id')
    def desk(self):
        return Desk
Enter fullscreen mode Exit fullscreen mode

This code will act the same as the code above. You will likely have to change this though to something like:

from orator.orm import has_one
from app.Desk import Desk

class User(Model):

    @has_one('id', 'user_desk')
    def desk(self):
        return Desk
Enter fullscreen mode Exit fullscreen mode

This will map the local column of user_desk to the foreign column of id in the desk table.

Dynamic Properties

We can use this relationship using "dynamic properties". This simply means that logically, you might think you should use the desk() method as a method but instead you will call it like a property:

def show(self):
    User.find(1).desk.computer
Enter fullscreen mode Exit fullscreen mode

Notice here that desk is not called by simply accessed like a property.

Belongs To

A belongs to relationship defines the inverse of a has_one relationship. Because it is the inverse of the relationship, we just need to flip the foreign keys

from orator.orm import belongs_to
from app.Desk import Desk

class User(Model):

    @belongs_to('user_desk', 'id')
    def desk(self):
        return Desk
Enter fullscreen mode Exit fullscreen mode

So now the map would be:

# local_key, other_table_key
@belongs_to('table_id', 'other_id')
Enter fullscreen mode Exit fullscreen mode

Has Many

You can also specify a has many relationship the same way. Any given user may have many tasks.

from orator.orm import has_many
from app.Task import Task

class User(Model):

    @has_many('user_desk', 'id')
    def desk(self):
        return Task
Enter fullscreen mode Exit fullscreen mode

So now the map would be:

# other_table_key, local_key
@has_many('other_id', 'table_id')
Enter fullscreen mode Exit fullscreen mode

You can read more about Model relationships if you need a more advanced relationship or more explanation at the Orator Documentation page.

Circular Dependency

Circular dependencies are not good in Python because it makes 2 classes rely on each other and importing 1 class may not be able to import another class in time and it will throw an exception of not being able to find the class it requires.

If two models rely on each other you may have a big issue on your hands when it comes to interacting with your models. If you are importing all your models at the top of each model, after a while you will be hit with these circular dependency issues. To get around that when building relationships, you should always lazy import:

from orator.orm import has_many

class User(Model):

    @has_many('user_desk', 'id')
    def desk(self):
        from app.Task import Task
        return Task
Enter fullscreen mode Exit fullscreen mode

This will alleviate any possibilities to have circular import issues.

Top comments (0)