Django Model Best Practices
Tags: django
Published: 2021-08-26 11:13:31

---
title: "Django Model Best Practices"
published: true
description: "I've collected and packed some data for you :)"
tags: django
cover_image: https://ugur.ozyilmazel.com/public/images/covers/ws-django.png
---

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](https://docs.djangoproject.com/en/3.2/internals/contributing/writing-code/coding-style/#model-style). Django documentation is your friend!

Doc tells that, there is a rule/order in the Model class:

1. All field declarations 
1. Custom manager attributes
1. `class Meta`
1. `def __str__()`
1. `def save()`
1. `def get_absolute_url()`
1. You custom methods, properties etc...

Here is an example:

```python
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.

```python
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:

```python
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:

```python
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;

```python
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`:

```python
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`:

```python
:
:
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.

```python
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](https://docs.djangoproject.com/en/3.2/ref/models/fields/#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:

```bash
models/
    __init__.py
    user.py
    post.py
    comment.py
```

In the `__init__.py` file:

```python
from .user import User
from .post import Post
:
:
```

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

```python
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:

```python
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:

```python
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!