Thought leadership from the most innovative tech companies, all in one place.

Create Reusable Models with Django and Mixins

Using mixins to create powerful, flexible Django models

image

I've been doing a much deeper dive into Django for web development lately and I have found myself repeating a lot of fields on a lot of tables. Things like created_at, updated_at, and description get repeated in a lot of my tables. The project in question is being ported from Java and Spring, where these different combinations of fields get handled by a wide variety of abstract classes.

In an effort to reduce the amount of code I have to write and to stay DRY, I decided to play around with mixins and see if I could use them to create reusable models that I could drop in wherever needed. After a bit of experimenting with getting this setup, I finally have a solution that I'm happy with that achieves this, and I would like to share that solution with you.

In this guide, I will be showing you how to use mixins to create powerful, flexible models in Django. As a word of note, I will be using Poetry as my dependency manager and Pyenv as my Python version manager. I will do Django guides with these in the future as they are quite powerful, however, the actual code implementation of the mixins is not impacted by this setup.

What is a Mixin?

In short, a mixin is a concept in Object-Oriented Programming that aims to solve part of the problem of multiple inheritance. The way I like to think about them is you are removing duplicated logic in multiple classes and separating them out into their own class. From there, they can be “mixed” in wherever you need.

Basically, they are classes that contain objects and properties that can be applied to other objects. There's a couple of important things to note about using them. First, mixins should contain concrete properties and logic. Classes that use them should not be overriding them or implementing them. The second important thing is that these classes are not supposed to stand on their own. You should not be instantiating them on. They are supposed to provide additional functionality to whatever classes use them. It's also important to note that mixins cannot be used in languages that don't support multiple inheritance, like Java or C#.

How are Mixins different from using Interfaces and Abstract classes?

Technically, using mixins is still a form of inheritance, multiple inheritance to be exact. In languages like C# or Java, you might create an interface or abstract class that your objects can inherit from and extend. Classes and abstract classes can provide methods and properties objects can reuse. Interfaces provide a contract that the object must abide by. Here's the catch though, classes in those languages can only extend one class. Whatever the parent class is, the child can only use methods and properties defined in the sole parent or override them. You could use an interface to express shared logic between classes, but you have to implement every single property and method in them every time you implement it.

Mixins solve this by being reusable blocks of code that are only meant to be used with other classes, not be implemented on their own. You are still using inheritance to create your classes, but you have a lot more freedom. You also avoid the “Diamond Problem” found in languages with multiple inheritance.

Basically, we can create small, reusable pieces of functionality that we can use with our classes.

Why use them in Django?

Mixins can provide a ton of functionality to not just your database models, but other classes you might have as well. Using them on models specifically lets you create easier, more manageable models. You can write your fields and methods as mixins and use them in any model you please. You can make mixins for a variety of use cases, like have a model be auditable or concurrent, or maybe you just need specific functionality for a group of models, say giving birds the ability to fly.

You also don't need to restrict yourself to expanding models. Say, for example, you have custom role checking logic to restrict route access. You could write a role checking mixin and use it in your views. It comes down to personal use cases.

As a whole, mixins help keep your code DRY and can help keep your objects cleaner. In general, if you find yourself repeating a lot of code, you might want to look for ways to abstract that logic out into its own class.

Show me some code

I am using Pyenv and Poetry to manage my Django projects. You are free to use virtual env, venv, pipenv, or whatever other tools you want, just know some commands might be a bit different. These tools in conjunction are extremely powerful and I would highly recommend trying them out if you find yourself fighting Python versions or virtual environments.

We are going to create a mixin that does basic model auditing. We want to keep track of who created and last updated the model, as well as when those actions happened. There are other fields you could add to this model, like version, but I prefer to keep that field in a concurrency mixin. This is purely programmer preference and you are free to do as you like!

Let's start by creating a new folder with

$ mdkir django-mixins && cd django-mixins

Let's check out our Python version and change it if need be

$ pyenv local
3.9.4

If that returns a version you don't want to use, just add the version at the end. If you don't have that version installed, the pyenv docs have a great guide on how to use it. Now let's set up the Poetry project with

$ poetry init

Just go through the setup process. You can either add Django right away as part of the installation or do it after. If you chose to do it after, you can run

$ poetry add django

To add it to your project. From here, it's just a standard Django project setup. I prefer having my settings and configuration have a different module name than just the project name, so I am going to run

$ poetry run django-admin startproject config .

The . on the end tells Django to create the project in this folder with the name of config. This is a personal preference, but I find it helps keep things cleaner.

Creating our core app

Let's create a really basic app called core. This app will contain any shared logic that our other Django apps might use.

$ poetry run django-admin startapp core

Open the models.py file and let's add in some basic models that our mixins can use

from django.db import models

class BaseModel(models.Model):
    id = models.AutoField(
    	primary_key=True,
        unique=True,
        editable=False
    )

    class Meta:
    	abstract = True


class BaseTimestampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class BaseUserTrackedModel(models.Model):
    created_by = models.EmailField()
    updated_by = models.EmailField()

    class Meta:
        abstract = True

I like creating these small, core models to reuse wherever I need them. One thing that might strike you as odd or overkill is the BaseModel class, where I am creating a model that only has an ID. This model is a simple, base definition that will be applied to every model we create. This is really convenient if you need any other meta-information or shared methods for every model, as well as giving fine-grained control over your ID field. If you use something other than AutoField, say BigAutoField, make sure you update settings.py to reflect this in the DEFAULT_AUTO_FIELD property. More can be found on that in the Django Docs.

Adding our Mixin

Now, let's add a new file to our core project called mixins.py. This file will contain our logic for any model mixins we might need. Let's create our audit mixin.

from core import models

class AuditModelMixin(models.BaseTimestampedModel,
                      models.BaseUserTrackedModel):

    """
    Mixin that provides fields created and updated at and by fields

    Includes
        - BaseTimestampedModel
        - BaseUserTrackedModel
    """
    class Meta:
        abstract = True

That is nice, DRY code! Whenever we want a model in another app to be audited now, all we need to do is

from django.db import models
from core.models import BaseModel
from core.mixins import AuditModelMixin
from  django.contrib.auth.models import AbstractUser


class Account(BaseModel,
              AuditModelMixin,
              AbstractUser):

    first_name = models.CharField(
        max_length=64,
        default='',
        blank=True
    )
    last_name = models.CharField(
        max_length=64,
        default='',
        blank=True
    )

    # Meta class, other fields, methods, etc

Here, we're creating an account class to represent a user of our application. BaseModel acts as our foundation layer for our domain models. AuditModelMixin will add any properties and methods from BaseTimestampedModel and BaseUserTrackedModel. AbstractUser is a generic, Django supplied user with fields related to authentication. When we run our database migrations, we should see our account model created in whatever database we have set up with the fields from our mixin.

$ poetry run python3 manage.py migrate
$ poetry run python3 manage.py makemigrations

Conclusion

In this guide, I gave you a rundown of what a mixin is and how to use it to extend your Django models. Mixins can be used outside of models too! They can be a great way to reuse bits of functionality all throughout your Django apps.

For more information on mixins this StackOverflow post provides a great example of using mixins in just normal Python code, not tied to Django.

You can find the source code for this article in this GitHub repository.

Thanks for reading!

You can also find this article on my blog here




Continue Learning