DEV Community

Cover image for Django Model Best Practices
Uğur "vigo" Özyılmazel
Uğur "vigo" Özyılmazel

Posted on • Updated on

Django Model Best Practices

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:

  1. All field declarations
  2. Custom manager attributes
  3. class Meta
  4. def __str__()
  5. def save()
  6. def get_absolute_url()
  7. 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,)

Enter fullscreen mode Exit fullscreen mode

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')
    :
    :
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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,
        :
        :
    )
Enter fullscreen mode Exit fullscreen mode

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'),
    )
Enter fullscreen mode Exit fullscreen mode

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'),
    )
    :
    :
Enter fullscreen mode Exit fullscreen mode

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
:
:
Enter fullscreen mode Exit fullscreen mode

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'),
    )
    :
    :
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the __init__.py file:

from .user import User
from .post import Post
:
:
Enter fullscreen mode Exit fullscreen mode

In the model file, I add __all__ = ['ModelName'];

from django.db import models

__all__ = ['City']

class City(models.Model):
    :
    :
Enter fullscreen mode Exit fullscreen mode

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'),
    )
    :
    :
Enter fullscreen mode Exit fullscreen mode

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]
    :
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (0)