Masonite ORM (Orator)

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

Introduction

Nearly every framework comes with some 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

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

or

$ pip install mysqlclient

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

$ pip install psycopg2

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': ''
    }
}

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': ''
    }
}

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)

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

Getting All Records

There's 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()

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

Getting by Value

We can also get by the 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', '[email protected]').get()

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', '[email protected]').first()

This will return the first result from the collection.

Aggregate Where

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

def show(self):
    User.where('id', '<' '2').get()

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')

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

Where In

You can use this method to get values were in a list of other values. For example, we can get all records whose emails are inside a list we provide:

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

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')
    )

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 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')
    )

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='[email protected]')

or we can pass in a dictionary:

def show(self):
    User.find(1).update({'user-email': '[email protected]'})

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 = '[email protected]'

    user.save()

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)

This will run the update on all records found.

Updating Timestamps

Sometimes you only want to update 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()

Creating New Records

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

Creating New Records With Properties

def show(self):
    user = User()
    user.name = 'Joe'
    user.email = '[email protected]'
    user.save()

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='[email protected]')

Deleting

We can also delete records:

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

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

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

# other_table_key, local_key 
@has_one('other_id', 'table_id')

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

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

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

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

So now the map would be:

# local_key, other_table_key
@belongs_to('table_id', 'other_id')

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

So now the map would be:

# other_table_key, local_key
@has_many('other_id', 'table_id')

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

This will alleviate any possibilities to have circular import issues.

Copyright Masonite 2019