Django Best Practices: Models

wsvincent

Will Vincent

Posted on February 21, 2020

Django Best Practices: Models

Properly defining database models is arguably the most important part of a new project, however Django provides us with tremendous flexibility around how we structure our models.

There is an official Django coding style which provides a recommended ordering for models.

  • choices
  • database fields
  • custom manager attributes
  • Meta
  • def __str__()
  • def save()
  • def get_absolute_url()
  • custom methods

But in practice it can be hard to go from a simple model to a massive one that you'd likely find in the real-world. In this tutorial we will create a progressively more complex University example to demonstrate building out a "best practice" Django model. You don't need to add all of the features we'll cover but it's good to be aware of them as these are the most common ones you'll see.

Step 1 - Model and Field Names

The simplest possible model has only 4 lines of code. It imports models, defines a model class University, and has a single field full_name.

# schools/models.py
from django.db import models

class University(models.Model):
    full_name = models.CharField(max_length=100)
Enter fullscreen mode Exit fullscreen mode

Models should always be Capitalized (eg. University, User, Article) and singular (eg. University not Universities) since they represents a single object, not multiple objects.

Fields should be all lowercase using underscores not camelCase so full_name not FullName.

Step 2 - Common Methods

After defining a model and fields, the next step is often adding __str__() and get_absolute_url methods.

The str method defines a human-readable representation of the model that is displayed in the Django admin site and in the Django shell.

The get_absolute_url() method sets a canonical URL for the model. This is required when using the reverse() function. It is also the correct way to refer to a model in your templates rather than hard-coding them.

<!-- BAD template code. Avoid! -->
<a href="/university/{{ object.id }}/">{{ object.full_name }}</a>

<!-- Correct -->
<a href="{{ object.get_absolute_url }}/">{{ object.full_name }}</a>
Enter fullscreen mode Exit fullscreen mode

And the Django admin will add a helpful "View on site" link to your models.

# schools/models.py
from django.db import models
from django.urls import reverse # new

class University(models.Model):
    full_name = models.CharField(max_length=100)

    def __str__(self): # new
        return self.full_name

    def get_absolute_url(self): # new
        return reverse('university_detail', args=[str(self.id)])
Enter fullscreen mode Exit fullscreen mode

Note: This example assumes a university_detail named URL referencing a detail view.

A third common method is save() method which lets us customize how a model is saved. A common example is for a blog app that needs to automatically set the author of a blog post to the current logged-in user. You'd implement that functionality with save().

Step 3 - Explicit Naming

Django makes a number of implicit naming choices that often trip up beginners. Since explicit is better than implicit, it's a good idea to set these values manually.

Django implicitly sets the human readable name for each field by converting underscores to spaces. So full_name is represented as full name. There are two ways to set this manually: you can use verbose_name() or as long as the field type is not ForeignKey, ManyToManyField, or OneToOneField, you can set it as an optional first positional argument.

Thus the following three lines of code are identical as far as Django is concerned:

# All 3 are equivalent!
full_name = models.CharField(max_length=100)
full_name = models.CharField('full name', max_length=100)
full_name = models.CharField(verbose_name='full name', max_length=100)
Enter fullscreen mode Exit fullscreen mode
# schools/models.py
from django.db import models


class University(models.Model):
    full_name = models.CharField(
        max_length=100,
        verbose_name='university full name', # new
    )

    def __str__(self):
        return self.full_name

    def get_absolute_url(self):
        return reverse('university_detail', args=[str(self.id)])
Enter fullscreen mode Exit fullscreen mode

When a ForeignKey relationship exists, Django again implicitly defines two more names we'll often need. Let's add a Student model to illustrate this.

# schools/models.py
...
class Student(models.Model): # new
    first_name = models.CharField('first name', max_length=30)
    last_name = models.CharField('last name', max_length=30)
    university = models.ForeignKey(
        University,
        on_delete=models.CASCADE,
        related_name='students',
        related_query_name='person',
    )

    def __str__(self):
        return '%s %s' % (self.first_name, self.last_name)

    def get_absolute_url(self):
        return reverse('student_detail', args=[str(self.id)])
Enter fullscreen mode Exit fullscreen mode

Working with a ForeignKey means following a relationship "backward". By default, Django sets this in our example to student_set and any filter names to student__first_name.

dartmouth = University.objects.get(full_name='Dartmouth')
dartmouth.student_set.all() # returns all students at Dartmouth
dartmouth.objects.filter(student__first_name='william') # returns all Dartmouth students named William
Enter fullscreen mode Exit fullscreen mode

Pay attention to the one underscore _ vs two underscores __ in this. It is an easy mistake to make!

A better approach is to explicitly set these values. related_name() should be the plural of the model containing the ForeignKey (students for the Student model) and related_query_name() should be singular.

yale = University.objects.get(full_name='Yale')
yale.students.all() # returns all students at Yale
yale.objects.filter(person__first_name='ashley') # returns all Yale students named Ashley
Enter fullscreen mode Exit fullscreen mode

Step 4 - Meta class

The Meta class is incredibly powerful and has a long list of features.

A good first step is to explicitly name your model too, not just your fields. This can be done with verbose_name and verbose_name_plural. Otherwise Django would just add an s to make it plural, universitys, which is wrong.

class Meta:
    verbose_name = 'university'
    verbose_name_plural = 'universities'
Enter fullscreen mode Exit fullscreen mode

Another common feature is ordering which sets the default order of a list of objects. If we wanted to have a webpage where we listed all universities alphabetically, this is how we'd set it.

Be aware though that there can be a performance hit to ordering results so don't order results if you don't need to.

However if you need ordered results, consider adding an index to speed things up. An index usually is a good idea but maintaining it also requires some overhead so you'll want to test which implementation is more performant.

To add an index we can either add a db_index=True to a field or set it via indexes in our Meta class. We'll implement the latter.

# schools/models.py
from django.db import models
from django.urls import reverse

class University(models.Model):
    full_name = models.CharField(
        max_length=100,
        verbose_name='university full name',
    )

    class Meta: # new
        indexes = [models.Index(fields=['full_name'])]
        ordering = ['-full_name']
        verbose_name = 'university'
        verbose_name_plural = 'universities'

    def __str__(self):
        return self.full_name

    def get_absolute_url(self):
        return reverse('university_detail', args=[str(self.id)])


class Student(models.Model):
    first_name = models.CharField('first name', max_length=30)
    last_name = models.CharField('last name', max_length=30)
    university = models.ForeignKey(
        University,
        on_delete=models.CASCADE,
        related_name='students',
        related_query_name='person',
    )

    class Meta: # new
        verbose_name = 'student'
        verbose_name_plural = 'students'

    def __str__(self):
        return '%s %s' % (self.first_name, self.last_name)

    def get_absolute_url(self):
        return reverse('student_detail', args=[str(self.id)])
Enter fullscreen mode Exit fullscreen mode

Step 5 - Choices

If choices are defined for a given model field, define each choice as a tuple of tuples, with an all-uppercase name as a class attribute on the model.

Here we've added choice fields to our University model and a new university_type field.

# schools/models.py
from django.db import models
from django.urls import reverse

class University(models.Model):
    UNIVERSITY_TYPE = ( # new
        ('PUBLIC', 'A public university'),
        ('PRIVATE', 'A private university')
      )

    full_name = models.CharField(
        verbose_name='university full name',
      max_length=100
    )
    university_type = models.CharField( # new
        choices=UNIVERSITY_TYPE_CHOICES,
        max_length=1,
        verbose_name='type of university',
    )

    class Meta:  
        indexes = [models.Index(fields=['full_name'])]
        ordering = ['-full_name']
        verbose_name = 'university'
        verbose_name_plural = 'universities'

    def __str__(self):
        return self.full_name

    def get_absolute_url(self):
        return reverse('university_detail', args=[str(self.id)])
Enter fullscreen mode Exit fullscreen mode

If you want to refer to the "human-readable" value of a choice field, use get_FOO_display().

Here's an example of adding and then displaying a new University in the Django shell.

>>> u = University(full_name='College of William and Mary', university_type='PUBLIC')
>>> u.save()
>>> u.university_type # 'PUBLIC'
>>> u.get_university_type_display() # 'A public university'
Enter fullscreen mode Exit fullscreen mode

If you're going to spend much time using the Django shell you'll like django-extensions and its shell_plus command.

Step 6 - Blank vs Null

By default whenever you define a new database model both null and blank are set to false. These two values are easy to confuse but quite different.

null is database-related. When a field has null=True it can store a database entry as NULL, meaning no value.

blank is validation-related, if blank=True then a form will allow an empty value, whereas if blank=False then a value is required.

The tricky part is that the field type dictates how to use these values. Do not use null with string-based fields like CharField or TextField as this leads to two possible values for "no data". The Django convention is instead to use the empty string '', not NULL.

An exception is that if CharField has both unique=True and blank=True then null=True is also required to avoid unique constraint violations when saving multiples objects with blank values.

Further Learning

With more time I'd like to cover custom methods, managers, and a fuller list of available field types. However for now this guide should hit the major points.

The book Two Scoops of Django has a very complete section on model design including an easy-to-use guide to best practices with blank and null. It is a recommended resource for all Django developers.

The website Simple Is Better Than Complex is a treasure trove of good tips on Django, including model design.

Haki Benita has a wonderful article on Bullet Proofing Django Models and Steel Kiwi also has a good list.

Note: Special thanks to Jeff Triplett and Patrick Cloke for feedback and suggestions.

💖 💪 🙅 🚩
wsvincent
Will Vincent

Posted on February 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related