I've been writing Django apps since 2008. Here are some Django Model tips that I've collected, applied over the years.
Model Writing Rules
Before you do anything, please visit and read this doc. Django documentation is your friend!
Doc tells that, there is a rule/order in the Model class:
- All field declarations
- Custom manager attributes
class Meta
def __str__()
def save()
def get_absolute_url()
- You custom methods, properties etc...
Here is an example:
def get_group_creator_sentinel():
payload = dict(
email='deleted@xxx.com',
first_name='Sentinel',
last_name='User',
)
return get_user_model().objects.get_or_create(**payload, defaults=payload)[0]
class Group(models.Model):
"""
User.objects.filter(group__name=...)
Permission.objects.filter(group__name=...)
"""
name = models.CharField(
max_length=150,
unique=True,
verbose_name=_('name'),
)
creator = models.ForeignKey(
to=settings.AUTH_USER_MODEL,
on_delete=models.SET(get_group_creator_sentinel),
related_name='creator_groups',
related_query_name='group',
verbose_name=_('creator'),
)
permissions = models.ManyToManyField(
to=Permission,
related_name='permissions_groups',
related_query_name='group',
verbose_name=_('permissions'),
blank=True,
)
objects = GroupManager()
class Meta:
app_label = 'core'
verbose_name = _('group')
verbose_name_plural = _('groups')
def __str__(self):
return self.name
def natural_key(self):
return (self.name,)
Correct Model Name
Model stands for a single element in the database. Model name should be singular. In some cases, Model name can be plural if you are building intermediate relations table since this is a different situation.
Some good model name examples:
Post
Article
User
Person
Set Table Names Manually
If possible, set the table name by hand in Meta
class. This helps you to have smaller table names and allows you to know database better.
class Customer(models.Model):
:
class Meta:
app_label = 'core'
db_table = 'customer'
verbose_name = _('customer')
verbose_name_plural = _('customers')
:
:
Don't Forget to Add created_at
and updated_at
Fields
In my projects, I always use a base.py
and put an abstract class for common usage:
class MyBaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
abstract = True
I sometimes add deleted_at
or is_active
if required.
Related Field Definition
I mostly collect all my models under a single app. This helps me to use short hand reference and keeps me avoiding circular imports.
With this usage style, I don't need to import models all the time! I use to='ModelName'
convention.
Let's say we have an Article
model and Article
model has a user field:
from django.conf import settings
class Article(models.Model):
user = models.ForeignKey(
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
:
:
)
or I have a City
model and City
has Country
model relation;
class City(models.Model):
country = models.ForeignKey(
to='Country',
on_delete=models.CASCADE,
related_name='cities',
related_query_name='city',
verbose_name=_('country'),
)
If you don't want to collect all models in a same app, you use to='app_name.Model'
too. Such as to='auth.Group'
...
Always Add related_name
and related_query_name
Options
If you ForeignKey
or ManyToMany
field, always set related_name
and related_query_name
:
class City(models.Model):
country = models.ForeignKey(
to='Country',
on_delete=models.CASCADE,
related_name='cities',
related_query_name='city',
verbose_name=_('country'),
)
:
:
From Country
instance, you can query cities via country.cities.filter()
. You can use in the lookups too: Country.objects.filter(city__name='xxx')
.
Never Use unique=True
for ForeignKey
Why? Because you have OneToOneField
for that!
Use NullBooleanField
Instead of models.BooleanField(null=True)
Use Model.DoesNotExist
Instead of ObjectDoesNotExist
:
:
:
try:
creator = user_model.objects.get(email=email)
except user_model.DoesNotExist as exc:
raise CommandError('email (%s) does not exists' % email) from exc
:
:
ObjectDoesNotExist
is useful when checking relational lookups over OneToOneField
!
choices
Usage
If you are oldskool like me, use this convention.
from django.utils.translation import gettext_lazy as _
from django.db import models
class Post(models.Model):
STATUS_OFFLINE = 0
STATUS_ONLINE = 1
STATUS_DELETED = 2
STATUS_DRAFT = 3
STATUS_CHOICES = (
(STATUS_OFFLINE, _('offline')),
(STATUS_ONLINE, _('online')),
(STATUS_DELETED, _('deleted')),
(STATUS_DRAFT, _('draft')),
)
status = models.IntegerField(
choices=STATUS_CHOICES,
default=STATUS_ONLINE,
verbose_name=_('status'),
)
:
:
Django has enumeration types now, you can take a look at it for different approach.
Better Field Names
If you have a User
model, do not add user_status
field. Make it shorter, just status
. No need to repeat user
word.
models/
Package Instead of models.py
When project grows, models.py
file becomes too long. I always put my models separately in a models package:
models/
__init__.py
user.py
post.py
comment.py
In the __init__.py
file:
from .user import User
from .post import Post
:
:
In the model file, I add __all__ = ['ModelName']
;
from django.db import models
__all__ = ['City']
class City(models.Model):
:
:
Always Use Your Own ManyToMany
Table
If you have ManyToMany
field in your model, Django handles everything for you. You have no control over that extra table, model, model's save method or migration.
What happens when you need to keep extra fields in that intermediate table? through
and through_fields
is your friend:
class Customer(models.Model):
name = models.CharField(max_length=100, verbose_name=_('name'))
users = models.ManyToManyField(
to=settings.AUTH_USER_MODEL,
through='CustomerMembership',
through_fields=('customer', 'user'),
related_name='customers',
related_query_name='customer',
blank=True,
verbose_name=_('users'),
)
:
:
class CustomerMembership(models.Model):
customer = models.ForeignKey(
to='Customer',
on_delete=models.CASCADE,
)
user = models.ForeignKey(
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
is_admin = models.BooleanField(
default=False,
verbose_name=_('customer admin status'),
)
:
:
Now we have full control over CustomerMembership
model. It's a regular model now! Django Admin also have great support for through operations:
class CustomerInlineAdmin(models.TabularInlineAdmin):
model = Customer.users.through
extra = 0
autocomplete_fields = ['customer', 'user']
:
:
class CustomerAdmin(admin.ModelAdmin):
list_display = ['__str__']
autocomplete_fields = ['users']
ordering = ['name']
inlines = [CustomerInlineAdmin]
:
Lastly, always consider/plan about your upcoming queries. Design your model against your future lookups. Maybe you need to filter/report your model for YEAR only. You need to make date lookup such as created_at__year=2021
or against month
or day
. Maybe it's better to add an integer field to hold year or month or day value? Querying an integer field will always be easy if you compare to date query...
I hope you enjoyed these tips & tricks! Happy programming!
Top comments (0)