Comment on page
Models
Models are the easiest way to interact with your tables. A model is a way for you to interact with a Python class in a simple and elegant way and have all the hard overhead stuff handled for you under the hood. A model can be used to query the data in the table or even create new records, fetch related records between tables and many other features.
The first step in using models is actually creating them. You can scaffold out a model by using the command:
$ python masonite-orm model Post
You can use the
--directory
flag to specify the location of these modelsThis will create a post model like so:
from masoniteorm.models import Model
class Post(Model):
"""Post Model"""
pass
From here you can do as basic or advanced queries as you want. You may need to configure your model based on your needs, though.
From here you can start querying your records:
user = User.first()
users = User.all()
active_users = User.where('active', 1).first()
We'll talk more about setting up your model below
Masonite ORM makes a few assumptions in order to have the easiest interface for your models.
The first is table names. Table names are assumed to be the plural of your model name. If you have a User model then the
users
table is assumed and if you have a model like Company
then the companies
table is assumed. You can realize that Masonite ORM is smart enough to know that the plural of Company
is not Companys
so don't worry about Masonite not being able to pick up your table name.If your table name is something other than the plural of your models you can change it using the
__table__
attribute:class Clients:
__table__ = "users"
The next thing Masonite assumes is the primary key. Masonite ORM assumes that the primary key name is
id
. You can change the primary key name easily:class Clients:
__primary_key__ = "user_id"
The next thing Masonite assumes is that you are using the
default
connection you setup in your configuration settings. You can also change this on the model:class Clients:
__connection__ = "staging"
By default, Masonite ORM protects against mass assignment to help prevent users from changing values on your tables you didn't want.
This is used in the create and update methods. You can set the columns you want to be mass assignable easily:
class Clients:
__fillable__ = ["email", "active", "password"]
Guarded attributes can be used to specify those columns which are not mass assignable. You can prevent some of the fields from being mass-assigned:
class Clients:
__guarded__ = ["password"]
Masonite also assumes you have
created_at
and updated_at
columns on your table. You can easily disable this behavior:class Clients:
__timestamps__ = False
Models use
UTC
as the default timezone. You can change the timezones on your models using the __timezone__
attribute:class User(Model):
__timezone__ = "Europe/Paris"
Almost all of a model's querying methods are passed off to the query builder. If you would like to see all the methods available for the query builder, see the QueryBuilder documentation here.
A query result will either have 1 or more records. If your model result has a single record then the result will be the model instance. You can then access attributes on that model instance. Here's an example:
from app.models.User import User
user = User.first()
user.name #== 'Joe'
user.email #== '[email protected]'
You can also get a record by its primary key:
from app.models.User import User
user = User.find(1)
user.name #== 'Joe'
user.email #== '[email protected]'
If your model result returns several results then it will be wrapped in a collection instance which you can use to iterate over:
from app.models.User import User
users = User.where('active', 1).get()
for user in users:
user.name #== 'Joe'
user.active #== '1'
user.email #== '[email protected]'
If you want to find a collection of records based on the models primary key you can pass a list to the
find
method:users = User.find([1,2,3])
for users in users:
user.name #== 'Joe'
user.active #== '1'
user.email #== '[email protected]'
The collection class also has some handy methods you can use to interact with your data:
user_emails = User.where('active', 1).get().pluck('email') #== Collection of email addresses
If you would like to see more methods available like
pluck
be sure to read the Collections documentation.You may also quickly delete records:
from app.models.User import User
user = User.delete(1)
This will delete the record based on the primary key value of 1.
You can also delete based on a query:
from app.models.User import User
user = User.where('active', 0).delete()
You may also use sub-queries to do more advanced queries using lambda expressions:
from app.models.User import User
users = User.where(lambda q: q.where('active', 1).where_null('deleted_at'))
# == SELECT * FROM `users` WHERE (`active` = '1' AND `deleted_at` IS NULL)
By default, Masonite ORM performs
SELECT *
queries. You can change this behavior in a few ways.The first way is to specify a
__selects__
attribute with a list of column names. You may use the as
keyword to alias your columns directly from this list:class Store(Model):
__selects__ = ["username", "administrator as is_admin"]
Now when you query your model, these selects will automatically be included:
store.all()
#== SELECT `username`, `administrator` as is_admin FROM `users`
Another way is directly on the
all()
method:store.all(["username", "administrator as is_admin"])
#== SELECT `username`, `administrator` as is_admin FROM `users`
This will also work on the
get
method as well:store.where("active", 1).get(["username", "administrator as is_admin"])
#== SELECT `username`, `administrator` as is_admin FROM `users` WHERE `active` = 1
Another great feature, when using models, is to be able to relate several models together (like how tables can relate to each other).
A belongs to relationship is a one-to-one relationship between 2 table records.
You can add a one-to-one relationship easily:
from masoniteorm.relationships import belongs_to
class User:
@belongs_to
def company(self):
from app.models.Company import Company
return Company
It will be assumed here that the primary key of the relationship here between users and companies is
{method_name}_id -> id
. You can change the relating columns if that is not the case:from masoniteorm.relationships import belongs_to
class User:
@belongs_to('company_id', 'primary_key_id')
def company(self):
from app.models.Company import Company
return Company
The first argument is always the column name on the current model's table and the second argument is the related field on the other table.
In addition to belongs to, you can define the inverse of a belongs to:
from masoniteorm.relationships import has_one
class User:
@has_one
def company(self):
from app.models.Company import Company
return Company
Note the keys here are flipped. This is the only relationship that has the keys reversed
from masoniteorm.relationships import has_one
class User:
@has_one('other_key', 'local_key')
def company(self):
from app.models.Company import Company
return Company
Another relationship is a one-to-many relationship where a record relates to many records, in another table:
from masoniteorm.relationships import has_many
class User:
@has_many('company_id', 'id')
def posts(self):
from app.models.Post import Post
return Post
The first argument is always the column name on the current model's table and the second argument is the related field on the other table.
When working with many to many relationships, there is a pivot table in between that we must account for. Masonite ORM will handle this pivot table for you entirely under the hood.
In a real world situation you may have a scenario where you have products and stores.
Stores can have many products and also products can be in many stores. For example, a store can sell a red shirt and a red shirt can be sold in many different stores.
In the database this may look something like this:
stores
-------
id
name
product_store
--------------
id
store_id
product_id
product
--------
id
name
Notice that there is a pivot table called
product_store
that is in between stores and products.We can use the
belongs_to_many
relationship to get all the products of a store easily. Let's start with the Store
model:from masoniteorm.models import Model
from masoniteorm.relationships import belongs_to_many
class Store(Model):
@belongs_to_many
def products(self):
from app.models.Product import Product
return Product
We can change the signature of the decorator to specify our foreign keys. In our example this would look like this:
from masoniteorm.models import Model
from masoniteorm.relationships import belongs_to_many
class Store(Model):
@belongs_to_many("store_id", "product_id", "id", "id")
def products(self):
from app.models.Product import Product
return Product
The first 2 keys are the foreign keys relating from stores to products through the pivot table and the last 2 keys are the foreign keys on the stores and products table.
If there are additional fields on your pivot table you need to fetch you can add the extra fields to the pivot record like so:
@belongs_to_many("store_id", "product_id", "id", "id", with_fields=['is_active'])
def products(self):
from app.models.Product import Product
return Product
This will fetch the additional fields on the pivot table which we have access to.
Once we create this relationship we can start querying from
stores
directly to products
:store = Store.find(1)
for product in store.products:
product.name #== Red Shirt
On each fetched record you can also get the pivot table and perform queries on it. This pivot record is the joining record inside the pivot table (
product_store
) where the store id and the product ID match. By default this attribute is pivot
.store = Store.find(1)
for product in store.products:
product.pivot.updated_at #== 2021-01-01
product.pivot.update({"updated_at": "2021-01-02"})
There are quite a few defaults that are created but there are ways to override them.
The first default is that the pivot table has a primary key called
id
. This is used to hydrate the record so you can update the pivot records. If you do not have a pivot primary key you can turn this feature off:@belongs_to_many(pivot_id=None)
You can also change the ID to something other than
id
:@belongs_to_many(pivot_id="other_column")
The next default is the name of the pivot table. The name of the pivot table is the singular form of both table names in alphabetical order. For example, if you are pivoting a
persons
table and a houses
table then the table name is assumed to be house_person
. You can change this naming:@belongs_to_many(table="home_ownership")
The next default is that there are no timestamps (
updated_at
and created_at
) on your pivot table. If you would like Masonite to manage timestamps you can:@belongs_to_many(with_timestamps=True)
The next default is that the pivot attribute on your model will be called
pivot
. You can change this:@belongs_to_many(attribute="ownerships")
Now when you need to get the pivot relationship you can do this through:
store = Store.find(1)
for product in store.products:
product.ownerships.updated_at #== 2021-01-01
product.ownerships.update({"updated_at": "2021-01-02"})
If you have timestamps on your pivot table, they must be called
created_at
and updated_at
.The
HasOneThrough
relationship defines a relationship between 2 tables through an intermediate table. For example, you might have a Shipment
that departs from a port and that Port
is located in a specific Country
.So therefore, a
Shipment
could be related to a specific Country
through a Port
.The schema would look something like this:
shipments
shipment_id - integer - PK
from_port_id - integer
ports
port_id - integer - PK
country_id - integer
name - string
countries
country_id - integer - PK
name - string
To create this type of relationship you simply need to import the relationship class and return a list with 2 models. The first model is the distant table you want to join. In this case we are joining a shipment to countries so we put the
Country
as the first list element. The second element is the intermediate table that we need to get from Shipment
to Country
. In this case that is the Port
model so we put that as the second element in the list.from masoniteorm.relationships import has_one_through
class Shipment(Model):
@has_one_through(
"port_id", # The foreign key on the ports table
"country_id", # The foreign key on the countries table
"from_port_id", # The local key on the shipments table
"country_id" # The local key on the ports table
)
def from_country(self):
from app.models.Country import Country
from app.models.Port import Port
return [Country, Port]
You can then use this relationship like any other relationship:
shipment = Shipment.find(1)
shipment.from_country.name #== China
shipment.with_("from_country").first() #== eager load
shipment.has("from_country").first() #== existance check
The
HasManyThrough
relationship defines a relationship between 2 tables through an intermediate table. For example, you might have a "user" that "likes" many "comments".So in model terms, a
User
could be related to multiple Comment
through a Like
.The schema would look something like this:
users
user_id - integer - PK
name - varchar
likes
like_id - integer - PK
user_id - integer - FK
comment_id - comment_id
comments
comment_id - integer - PK
body - text
To create this type of relationship you simply need to import the relationship class and return a list with 2 models. The first model is the distant table you want to join. In this case we are joining a user to comments so we put the
Comment
as the first list element. The second element is the intermediate table that we need to get from User
to Comment
. In this case that is the Like
model so we put that as the second element in the list.from masoniteorm.relationships import has_many_through
class User(Model):
@has_many_through(
"like_id", # The foreign key on the intermediate (likes) table
"comment_id", # The foreign key on the distant (comments) table
"user_id", # The local key on the local (users) table
"user_id" # The local key on the intermediate (likes) table
)
def liked_comments(self):
from app.models.Comment import Comment
from app.models.Like import Like
return [Comment, Like]
You can then use this relationship like any other relationship:
user = User.find(1)
for comment in user.liked_comments:
comment.body
user.with_("liked_comments").first() #== eager load comments
user.has("liked_comments").first() #== all users who have liked comments
You can easily use relationships to get those related records. Here is an example on how to get the company record:
user = User.first()
user.company #== <app.models.Company>
user.company.name #== Masonite X Inc.
for post in user.posts:
post.title
Sometimes you want to be able to get the related query and append on to it on the fly.
For example, you may have a
User
and Phone
relationship that looks like this:class User(Model):
@has_many
def phones(self):
return Phone
On the fly you may want to only get the active phones. You can do this by using the
related()
method on a model instance.user = User.find(1)
# All users phones
phones = user.phones
# All active users phones
active_phones = user.related("phones").where("active", 1).get()
The
with_count
method can be used to get the number of records in a relationship.If you want to fetch the number of permissions a role has for example:
Role.with_count('permissions').get()
This will return a collection on each record with the
{relationship}_count
attribute. You can get this attribute like this:roles = Role.with_count('permissions').get()
for role in roles:
role.permissions_count #== 7
The method also works for single records
roles = Role.with_count('permissions').find(1).permissions_count #== 7
You may also optionally pass in a lambda function as a callable to pass in an additional query filter against the relationship
Role.with_count(
'permissions',
lambda q: (
q.where_like("name", "%Creates%")
)
Sometimes you'll need to get all records where a record has (or doesnt_have) a related record.
For example, you may want to get all users that have addresses:
users = User.has("addresses").get()
Or you may want to get all users that don't have addresses:
users = User.doesnt_have("addresses").get()
You can also perform another query on the relationship. For example, you may want all users that have addresses in the state of NY:
users = User.where_has("addresses", lambda query: (
query.where("state", "NY")
)).get()
You may do this also with
where_doesnt_have
to get all users where they don't have addresses in the state of NY:users = User.where_doesnt_have("addresses", lambda query: (
query.where("state", "NY")
)).get()
You may also perform nested existence checks such as:
articles = Article.has("author.addresses", lambda query: (
query.where("state", "NY")
)).get()
This would be how you would get all articles where the authors have addresses in NY.
You also have the full support of doing OR conditionals by prefixing any of the methods with
or_
:or_has
or_where_has
or_doesnt_have
or_where_doesnt_have
Polymorphic relationships are when a single row can have a relationship to any other table. For example, a
Like
could be associated to a Comment
or an Article
.On a polymorphic table, we typically have a record_type that repesents a table (or a model) and a record_id which represents the primary key value of the related table. Masonite ORM needs to know which record_type maps to which model.
We will create this map on our connection resolver. This is typically the
DB
variable in your database config file:DB = ConnectionResolver().set_connection_details(DATABASES)
# ...
DB.morph_map({
"Article": Article,
"Comment": Comment,
})
When setting up a polymorphic relation it is very similiar to a normal relationship. The major difference is that you will have multiple models pointing to a single polymorphic table. In a polymorphic one-to-one relationship setup you would have a table setup like this:
comments
- comment_id - PK
- description - Varchar
article:
- article_id - PK
- title - Varchar
images
- id - PK
- record_type - Varchar
- record_id - Unsigned Int
Notice the
images
table has record_type
and record_id
fields. These could be named anything but it should contain a varchar type column that will be used to map to a model as well as a column to put the foreign tables primary key value.The models setup would look like this:
from masoniteorm.relationships import morph_to, morph_many
class Image(Model):
@morph_to
def record(self):
return
class Article(Model):
@morph_one("record_type", "record_id")
def image(self):
return Like
class Comment(Model):
@morph_one("record_type", "record_id")
def image(self):
return Like
When setting up a polymorphic relation it is very similiar to a normal relationship. The major difference is that you will have multiple models pointing to a single polymorphic table. In a polymorphic one-to-many relationship setup you would have a table setup like this:
comments
- comment_id - PK
- description - Varchar
articles:
- article_id - PK
- title - Varchar
likes
- id - PK
- record_type - Varchar
- record_id - Unsigned Int
Notice the
likes
table has record_type
and record_id
fields. These could be named anything but it should contain a varchar type column that will be used to map to a model as well as a column to put the foreign tables primary key value.In this case the
likes
table still has one
relationship to multiple models but the relating tables ("articles" and "comments" has many
records to the likes
table).The models setup would look like this:
from masoniteorm.relationships import morph_to, morph_many
class Likes(Model):
@morph_to
def record(self):
return
class Article(Model):
@morph_many("record_type", "record_id")
def likes(self):
return Like
class Comment(Model):
@morph_many("record_type", "record_id")
def likes(self):
return Like
Masonite ORM has
morph_to
and a morph_to_many
relationships. This is used to relate multiple records to the polymorphic table. These relationships are used on the polymorphic model to relate to the related models. The morph_to
will return 1 result from the related model and the morph_to_many
would return multiple.The model example would look like this:
from masoniteorm.relationships import morph_to, morph_many
class Likes(Model):
@morph_to
def record(self):
return
class User(Model):
@morph_to_many
def record(self):
return
You can eager load any related records. Eager loading is when you preload model results instead of calling the database each time.
Let's take the example of fetching a user's phone:
users = User.all()
for user in users:
user.phone
This will result in the query:
SELECT * FROM users
SELECT * FROM phones where user_id = 1
SELECT * FROM phones where user_id = 2
SELECT * FROM phones where user_id = 3
SELECT * FROM phones where user_id = 4
...
This will result in a lot of database calls. Now let's take a look at the same example but with eager loading:
users = User.with_('phone').get()
for user in users:
user.phone
This would now result in this query:
SELECT * FROM users
SELECT * FROM phones where user_id IN (1, 2, 3, 4)
This resulted in only 2 queries. Any subsquent calls will pull in the result from the eager loaded result set.
You can also default all model calls with eager loading by using the
__with__
attribute on the model:from masoniteorm.models import Model
from masoniteorm.relationships import belongs_to_many
class Store(Model):
__with__ = ['products']
@belongs_to_many
def products(self):
from app.models.Product import Product
return Product
You can change the relationship query that is ran on the fly using a dictionary and a lambda expression:
For example if you wanted to eager only the users phones that are activated:
users = User.with_({
'phone': lambda q: q.where("activated", 1)
}).get()
for user in users:
user.phone
You can use the with_ method in addition to other eager loads:
users = User.with_("friends", "cars", {
'phone': lambda q: q.where("activated", 1)
}).get()
for user in users:
user.phone
You may also eager load multiple relationships. Let's take another more advanced example...
Let's say you would like to get a user's phone as well as their contacts. The code would look like this:
users = User.all()
for user in users:
for contact in user.phone:
contact.name
This would result in the query:
SELECT * FROM users
SELECT * FROM phones where user_id = 1
SELECT * from contacts where phone_id = 30
SELECT * FROM phones where user_id = 2
SELECT * from contacts where phone_id = 31
SELECT * FROM phones where user_id = 3
SELECT * from contacts where phone_id = 32
SELECT * FROM phones where user_id = 4
SELECT * from contacts where phone_id = 33
...
You can see how this can get pretty large as we are looping through hundreds of users.
We can use nested eager loading to solve this by specifying the chain of relationships using
.
notation:users = User.with_('phone.contacts').all()
for user in users:
for contact in user.phone:
contact.name
This would now result in the query:
SELECT * FROM users
SELECT * FROM phones where user_id IN (1,2,3,4)
SELECT * from contacts where phone_id IN (30, 31, 32, 33)
You can see how this would result in 3 queries no matter how many users you had.
If you have relationships on your models you can easily join them:
If you have a model that like this:
from masoniteorm.relationships import has_many
class User:
@has_many('company_id', 'id')
def posts(self):
from app.models.Post import Post
return Post
You can use the
joins
method:User.joins('posts')
This will build out the
join
method.You can also specify the clause of the join (inner, left, right). The default is an inner join
User.joins('posts', clause="right")
Additionally if you want to specify additional where clauses you can use the
join_on
method:User.join_on('posts', lambda q: (
q.where('active', 1)
))
Scopes are a way to take common queries you may be doing and condense them into a method where you can then chain onto them. Let's say you are doing a query like getting the active user frequently:
user = User.where('active', 1).get()
We can take this query and add it as a scope:
from masoniteorm.scopes import scope
class User(Model):
@scope
def active(self, query):
return query.where('active', 1)
Now we can simply call the active method:
user = User.active().get()
You may also pass in arguments:
from masoniteorm.scopes import scope
class User(Model):
@scope
def active(self, query, active_or_inactive):
return query.where('active', active_or_inactive)
then pass an argument to it:
user = User.active(1).get()
user = User.active(0).get()
Masonite ORM also comes with a global scope to enable soft deleting for your models.
Simply inherit the
SoftDeletesMixin
scope class:from masoniteorm.scopes import SoftDeletesMixin
class User(Model, SoftDeletesMixin):
# ..
Now whenever you delete a record, instead of deleting it it will update the
deleted_at
record from the table to the current timestamp:User.where("id", 1).delete()
# == UPDATE `users` SET `deleted_at` = '2020-01-01 10:00:00' WHERE `id` = 1
When you fetch records it will also only fetch undeleted records:
User.all() #== SELECT * FROM `users` WHERE `deleted_at` IS NULL
You can disable this behavior as well:
User.with_trashed().all() #== SELECT * FROM `users`
You can also get only the deleted records:
User.only_trashed().all() #== SELECT * FROM `users` WHERE `deleted_at` IS NOT NULL
You can also restore records:
User.where('admin', 1).restore() #== UPDATE `users` SET `deleted_at` = NULL WHERE `admin` = '1'
Lastly, you can override this behavior and force the delete query:
User.where('admin', 1).force_delete() #== DELETE FROM `users` WHERE `admin` = '1'