ADVANCED DJANGO MODELS: IMPROVE YOUR PYTHON DEVELOPMENT

INTRODUCTION

Models are a core concept of the Django framework. According to Django’s design philosophies for models, we should be as explicit as possible with the naming and functionality of our fields, and ensure that we’re including all relevant functionality related to our model in the model itself, rather than in the views or somewhere else. If you’ve worked with Ruby on Rails before, these design philosophies won’t seem new as both Rails and Django implement the Active Record pattern for their object-relational mapping (ORM) systems to handle stored data.

In this post we’ll look at some ways to leverage these philosophies, core Django features, and even some libraries to help make our models better.

getter/setter/deleter PROPERTIES

As a feature of Python since version 2.2, a property’s usage looks like an attribute but is actually a method. While using a property on a model isn’t that advanced, we can use some underutilized features of the Python property to make our models more powerful.

If you’re using Django’s built-in authentication or have customized your authentication using AbstractBaseUser, you’re probably familiar with the last_login field defined on the User model, which is a saved timestamp of the user’s last login to your application. If we want to use last_login, but also have a field named last_seen saved to a cache more frequently, we could do so pretty easily.

First, we’ll make a Python property that finds a value in the cache, and if it can’t, it returns the value from the database.

accounts/models.py

from django.contrib.auth.base_user import AbstractBaseUser
from django.core.cache import cache


class User(AbstractBaseUser):
    ...
    
    @property
    def last_seen(self):
        """
        Returns the 'last_seen' value from the cache for a User.
        """
        last_seen = cache.get('last_seen_{0}'.format(self.pk))

        # Check cache result, otherwise return the database value
        if last_seen:
            return last_seen

        return self.last_login

Note: I’ve slimmed the model down a bit as there’s a separate tutorial on this blog about specifically customizing the built-in Django user model.

The property above checks our cache for the user’s last_seen value, and if it doesn’t find anything, it will return the user’s stored last_login value from the model. Referencing <instance>.last_seen now provides a much more customizable attribute on our model behind a very simple interface.

We can expand this to include custom behavior when a value is assigned to our property (some_user.last_seen = some_date_time), or when a value is deleted from the property (del some_user.last_seen).

...
    
@last_seen.setter
def last_seen(self, value):
    """
    Sets the 'last_seen_[uuid]' value in the cache for a User.
    """
    now = value

    # Save in the cache
    cache.set('last_seen_{0}'.format(self.pk), now)

@last_seen.deleter
def last_seen(self):
    """
    Removes the 'last_seen' value from the cache.
    """
    # Delete the cache key
    cache.delete('last_seen_{0}'.format(self.pk))
    
...

Now, whenever a value is assigned to our last_seen property, we save it to the cache, and when a value is removed with del, we remove it from the cache. Using setter and deleter is described in the Python documentation but is rarely seen in the wild when looking at Django models.

You may have a use case like this one, where you want to store something that doesn’t necessarily need to be persisted to a traditional database, or for performance reasons, shouldn’t be. Using a custom property like the above example is a great solution.

In a similar use case, the python-social-auth library, a tool for managing user authentication using third-party platforms like GitHub and Twitter, will create and manage updating information in your database based on information from the platform the user logged-in with. In some cases, the information returned won’t match the fields in our database. For example, the python-social-auth library will pass a fullname keyword argument when creating the user. If, perhaps in our database, we used full_name as our attribute name then we might be in a pinch.

A simple way around this is by using the getter/setter pattern from above:

@property
def fullname(self) -> str:
    return self.full_name

@fullname.setter
def fullname(self, value: str):
    self.full_name = value

Now, when python-social-auth saves a user’s fullname to our model (new_user.fullname = 'Some User'), we’ll intercept it and save it to our database field, full_name, instead.

through MODEL RELATIONSHIPS

Django’s many-to-many relationships are a great way of handling complex object relationships simply, but they don’t afford us the ability to add custom attributes to the intermediate models they create. By default, this simply includes an identifier and two foreign key references to join the objects together.

Using the Django ManyToManyField through parameter, we can create this intermediate model ourselves and add any additional fields we deem necessary.

If our application, for example, not only needed users to have memberships within groups, but wanted to track when that membership started, we could use a custom intermediate model to do so.

accounts/models.py

import uuid

from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.utils.timezone import now


class User(AbstractBaseUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    …

class Group(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    members = models.ManyToManyField(User, through='Membership')

class Membership(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    joined = models.DateTimeField(editable=False, default=now)

In the example above, we’re still using a ManyToManyField to handle the relationship between a user and a group, but by passing the Membership model using the through keyword argument, we can now add our joined custom attribute to the model to track when the group membership was started. This through model is a standard Django model, it just requires a primary key (we use UUIDs here), and two foreign keys to join the objects together.

Using the same three model pattern, we could create a simple subscription database for our site:

import uuid

from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.utils.timezone import now


class User(AbstractBaseUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    ...

class Plan(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=50, unique=True, default='free')
    subscribers = models.ManyToManyField(User, through='Subscription', related_name='subscriptions', related_query_name='subscriptions')

class Subscription(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    plan = models.ForeignKey(Plan, on_delete=models.CASCADE)
    created = models.DateTimeField(editable=False, default=now)
    updated = models.DateTimeField(auto_now=True)
    cancelled = models.DateTimeField(blank=True, null=True)

Here we’re able to track when a user first subscribed, when they updated their subscription, and if we added the code paths for it, when a user canceled their subscription to our application.

Using through models with the ManyToManyField is a great way to add more data to our intermediate models and provide a more thorough experience for our users without much added work.

PROXY MODELS

Normally in Django, when you subclass a model (this doesn’t include abstract models) into a new class, the framework will create new database tables for that class and link them (via OneToOneField) to the parent database tables. Django calls this “multi-table inheritance” and it’s a great way to re-use existing model fields and structures and add your own data to them. “Don’t repeat yourself,” as the Django design philosophies state.

Multi-table inheritance example:

from django.db import models

class Vehicle(models.Model):
    model = models.CharField(max_length=50)
    manufacturer = models.CharField(max_length=80)
    year = models.IntegerField(max_length=4)

class Airplane(Vehicle):
    is_cargo = models.BooleanField(default=False)
    is_passenger = models.BooleanField(default=True)

This example would create both vehicles_vehicle and vehicles_airplane database tables,linked with foreign keys. This allows us to leverage the existing data that lives inside vehicles_vehicle, while adding our own vehicle specific attributes to each subclass, vehicle_airplane, in this case.

In some use cases, we may not need to store extra data at all. Instead, we could change some of the parent model’s behavior, maybe by adding a method, property, or model manager. This is where proxy models shine. Proxy models allow us to change the Python behavior of a model without changing the database.

vehicles/models.py

from django.db import models

class Car(models.Model):
    vin = models.CharField(max_length=17)
    model = models.CharField(max_length=50)
    manufacturer = models.CharField(max_length=80)
    year = models.IntegerField(max_length=4)
    ...

class HondaManager(models.Manager):
    def get_queryset(self):
        return super(HondaManager, self).get_queryset().filter(model='Honda')

class Honda(Car):
    objects = HondaManager()
    
    class Meta:
        proxy = True
    
    @property
    def is_domestic(self):
        return False
    
    def get_honda_service_logs(self):
        ...

Proxy models are declared just like normal models. In our example, we tell Django that Honda is a proxy model by setting the proxy attribute of the Honda Meta class to True. I’ve added a property and a method stub example, but you can see we’ve added a custom model manager to our Honda proxy model.

This ensures that whenever we request objects from the database using our Honda model, we get only Car instances back where model= 'Honda'. Proxy models make it easy for us to quickly add customizations on top of existing models using the same data. If we were to delete, create, or update any Car instance using our Honda model or manager, it would be saved into the vehicles_car database just as if we were using the parent (Car) class.

WRAP UP

If you’re already comfortable working in Python classes, then you’ll feel right at home with Django’s models: inheritance, multiple inheritance, method overrides, and introspection. These models are all part of how the Django object-relational mapper was designed.

Multi-table inheritance and manually defining intermediate tables for SQL joins aren’t necessarily basic concepts, but are implemented simply with a bit of Django and Python knowh-ow. Being able to leverage features of the language and framework alongside one another is one of the reasons Django is a popular web framework.

For further reading, check out Django’s documentation topic for models. The second half of the page covers some of what we’ve covered here and more– I’m always learning something new when I read a page of their high-level topics.

This article originally appeared on Kite.

Nobody had feels yet. And how do you feel?
0 :thumbsup: Thumbsup
0 :heart: Heart
0 :joy: Joy
0 :heart_eyes: Heart_eyes
0 :blush: Blush
0 :cry: Cry
0 :rage: Rage

Add Comment

* Checkbox GDPR is required

*

I agree

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Bad Behavior has blocked 159 access attempts in the last 7 days.