Django signals VS overriding the model `save` VS overriding the manager `create` method

Django provides a rich set of built-in tools to customize its ORM (Object-Relational Mapping) behavior. Among the most common ones are signals, overriding the save method, and customizing the ObjectManager’s create method.

Overriding the model save method

Syntax

# In my_app.models.
from django.db import models
from django.utils import timezone


class MyModel(models.Model):

    date_updated = models.DateTimeField(blank=True, null=True)

    def save(self, *args, **kwargs):
        self.date_updated = timezone.now()
        print('The instance is about to get saved, and now has a date updated set!')
        super().save(*args, **kwargs)

Pros

  • Explicitness: Overriding the save method has the benefit of being particularly readable: you can see all the logic linked with that model within the model module.
  • Simple to implement: Just extend the save method and add your custom logic!

Cons

  • Not invoked on bulk operations: Bulk operations like update() bypass the save method.

Documentation

Django’s documentation for model methods

Implementing signals in Django

Syntax

# In my_app.signals.
from django.db.models.signals import pre_save
from django.utils import timezone

from my_app.models import MyModel

@receiver(pre_save, sender=MyModel)
def set_date_updated(sender, instance, raw, using, update_fields, **kwargs):
    instance.date_updated = timezone.now()
    print('The instance is about to get saved, and now has a date updated set!')

Warning: make sure you import your signals in your apps.py file!

Pros

  • Reusability: Signals can easily be reused across multiple models, thereby avoiding redundancy.
  • Global availability: Signals are especially convenient to handle interactions across models (for instance, they can solve some circular imports).
  • With fixtures: save() method is not executed while loading fixtures in unit test mode, whereas pre/post-save signals are. So if you want to test the way you’re cleaning inputs, you can only do so with signals.
  • With M2M fields: .save() doesn’t have access to the M2M field changes, whereas the save of M2M fields can be controlled through dedicated signals.

Cons

  • Debugging complexity: Signals can make debugging difficult since they’re not tied to a specific part of your application.
  • Performance overhead: Using multiple signals might add to the processing overhead.

Documentation

Django’s documentation about signals

Overriding ObjectManager create Method

Syntax

class MyModelManager(models.Manager):
    def create(self, **kwargs):
        kwargs['date_updated'] = timezone.now()
        print('The instance is about to get created, and will have a date updated already set!')
        return super().create(**kwargs)

N.B. Don’t forget to declare your custom model manager in the model class:

class MyModel(models.Model):
    objects = MyModelManager

Pros

  • Explicit behavior on object creation: This allows you to apply custom logic specifically during object creation.
  • Fine-grained control: Allows more control over the object initialization process. For instance, the object manager create method can take extra arguments that are not model attributes (just pop them before calling super().create).

Cons

  • Limited to programmatic object creation: Only invoked when you use the manager’s create method, and is not even executed when adding instances via the admin forms, for example!

Conclusion and recommendations

  • Use signals for global, cross-cutting concerns, or to track M2M saves.
  • Override the model save method for business logic that is model-specific, and should be executed on every save action.
  • Override the ObjectManager’s create method for logic that should only happen at the time of object creation, and/or if you take an extra variable that should not be passed to the model downstream.

Note

None of these are executed during migrations, though, as Django uses “frozen” models during migrations to make sure they’re reproducible at any moment.