Django Signals - Model Save and Signal Flows
Since I usually forget the implementations of model signals, I’d like to write a post to provide a clear and accessible reference. So I’ve included the original code snippets for model signals directly from the Django 4.2 branch. Permalinks to the specific source code locations are also provided for further exploration.
Intro
When working with Django models, understanding how the save()
and
save_base()
methods work alongside signals is crucial for proper model
handling.
Django uses a two-level approach for saving model instances:
save()
- The high-level method that you typically callsave_base()
- The lower-level method that handles the actual saving logic
Let’s break down the actual implementations and understand what happens under the hood.
save
The save()
method in Django models has several important steps before it eventually calls save_base()
. Here’s the detailed flow:
Parameter Processing
def save(self, *args, force_insert=False, force_update=False, using=None, update_fields=None):
force_insert
: Forces an SQL INSERTforce_update
: Forces an SQL UPDATEusing
: Specifies which database to useupdate_fields
: Specifies which fields to update
Related Fields Preparation
self._prepare_related_fields_for_save(operation_name="save")
This prepares any related fields that need to be handled during the save operation.
Database Selection
using = using or router.db_for_write(self.__class__, instance=self)
If no database is specified, Django determines which database to write to.
Validation Checks
if force_insert and (force_update or update_fields): raise ValueError("Cannot force both insert and updating in model saving.")
Ensures that conflicting options aren’t specified.
Deferred Fields Handling
deferred_non_generated_fields = { f.attname for f in self._meta.concrete_fields if f.attname not in self.__dict__ and f.generated is False }
Identifies which fields were deferred and not loaded.
Update Fields Processing
if update_fields is not None: if not update_fields: return update_fields = frozenset(update_fields) field_names = self._meta._non_pk_concrete_field_names non_model_fields = update_fields.difference(field_names)
- Validates update_fields if specified
- Returns early if update_fields is empty
- Checks for invalid field names
Automatic Update Fields Detection
elif (not force_insert and deferred_non_generated_fields and using == self._state.db): field_names = set() for field in self._meta.concrete_fields: if not field.primary_key and not hasattr(field, "through"): field_names.add(field.attname) loaded_fields = field_names.difference(deferred_non_generated_fields) if loaded_fields: update_fields = frozenset(loaded_fields)
Automatically determines which fields need updating for deferred models.
Final save_base() Call
self.save_base( using=using, force_insert=force_insert, force_update=force_update, update_fields=update_fields, )
Delegates to
save_base()
for the actual saving operation.
Here is the full code, save - permalink:
# django/django/db/models/base.py
class Model(AltersData, metaclass=ModelBase):
...
def save(
self,
*args,
force_insert=False,
force_update=False,
using=None,
update_fields=None,
):
"""
Save the current instance. Override this in a subclass if you want to
control the saving process.
The 'force_insert' and 'force_update' parameters can be used to insist
that the "save" must be an SQL insert or update (or equivalent for
non-SQL backends), respectively. Normally, they should not be set.
"""
# RemovedInDjango60Warning.
if args:
force_insert, force_update, using, update_fields = self._parse_save_params(
*args,
method_name="save",
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
self._prepare_related_fields_for_save(operation_name="save")
using = using or router.db_for_write(self.__class__, instance=self)
if force_insert and (force_update or update_fields):
raise ValueError("Cannot force both insert and updating in model saving.")
deferred_non_generated_fields = {
f.attname for f in self._meta.concrete_fields if f.attname not in self.__dict__ and f.generated is False
}
if update_fields is not None:
# If update_fields is empty, skip the save. We do also check for
# no-op saves later on for inheritance cases. This bailout is
# still needed for skipping signal sending.
if not update_fields:
return
update_fields = frozenset(update_fields)
field_names = self._meta._non_pk_concrete_field_names
non_model_fields = update_fields.difference(field_names)
if non_model_fields:
raise ValueError(
"The following fields do not exist in this model, are m2m "
"fields, or are non-concrete fields: %s" % ", ".join(non_model_fields)
)
# If saving to the same database, and this model is deferred, then
# automatically do an "update_fields" save on the loaded fields.
elif not force_insert and deferred_non_generated_fields and using == self._state.db:
field_names = set()
for field in self._meta.concrete_fields:
if not field.primary_key and not hasattr(field, "through"):
field_names.add(field.attname)
loaded_fields = field_names.difference(deferred_non_generated_fields)
if loaded_fields:
update_fields = frozenset(loaded_fields)
self.save_base(
using=using,
force_insert=force_insert,
force_update=force_update,
update_fields=update_fields,
)
save_base
The save_base()
method is Django’s core saving mechanism that handles the
actual database operations. Let’s break down its implementation step by step.
- Parameter Processing
def save_base(
self,
raw=False,
force_insert=False,
force_update=False,
using=None,
update_fields=None,
):
raw
: Skip parent model saving and value modifications (used in fixture loading)force_insert
: Force an SQL INSERTforce_update
: Force an SQL UPDATEusing
: Database to useupdate_fields
: Specific fields to update
Database Selection
using = using or router.db_for_write(self.__class__, instance=self)
Determines which database to write to if not specified.
Parameter Validation
assert not (force_insert and (force_update or update_fields)) assert update_fields is None or update_fields
Ensures:
- Can’t force both insert and update
- update_fields can’t be empty if specified
Class Resolution
cls = origin = self.__class__ if cls._meta.proxy: cls = cls._meta.concrete_model meta = cls._meta
- Stores original class
- Handles proxy models by getting concrete model
- Gets model metadata
Pre-save Signal
if not meta.auto_created: pre_save.send( sender=origin, instance=self, raw=raw, using=using, update_fields=update_fields, )
Sends pre_save signal if model isn’t auto-created.
Transaction Management
if meta.parents: context_manager = transaction.atomic(using=using, savepoint=False) else: context_manager = transaction.mark_for_rollback_on_error(using=using)
- Uses atomic transaction for models with parents
- Marks for rollback on error for others
Main Save Operation
with context_manager: parent_inserted = False if not raw: force_insert = self._validate_force_insert(force_insert) parent_inserted = self._save_parents( cls, using, update_fields, force_insert ) updated = self._save_table( raw, cls, force_insert or parent_inserted, force_update, using, update_fields, )
Inside transaction:
- Validates force_insert
- Saves parent models if needed
- Performs actual table save
State Updates
self._state.db = using self._state.adding = False
Updates instance state:
- Records which database was used
- Marks instance as no longer new
Post-save Signal
if not meta.auto_created: post_save.send( sender=origin, instance=self, created=(not updated), update_fields=update_fields, raw=raw, using=using, )
Sends post_save signal with created flag.
Remember that save_base()
is typically called by save()
and rarely needs to
be called directly unless you’re implementing custom save behavior.
Here is the full code, base_save - permalink:
# django/django/db/models/base.py
class Model(AltersData, metaclass=ModelBase):
...
def save_base(
self,
raw=False,
force_insert=False,
force_update=False,
using=None,
update_fields=None,
):
"""
Handle the parts of saving which should be done only once per save,
yet need to be done in raw saves, too. This includes some sanity
checks and signal sending.
The 'raw' argument is telling save_base not to save any parent
models and not to do any changes to the values before save. This
is used by fixture loading.
"""
using = using or router.db_for_write(self.__class__, instance=self)
assert not (force_insert and (force_update or update_fields))
assert update_fields is None or update_fields
cls = origin = self.__class__
# Skip proxies, but keep the origin as the proxy model.
if cls._meta.proxy:
cls = cls._meta.concrete_model
meta = cls._meta
if not meta.auto_created:
pre_save.send(
sender=origin,
instance=self,
raw=raw,
using=using,
update_fields=update_fields,
)
# A transaction isn't needed if one query is issued.
if meta.parents:
context_manager = transaction.atomic(using=using, savepoint=False)
else:
context_manager = transaction.mark_for_rollback_on_error(using=using)
with context_manager:
parent_inserted = False
if not raw:
# Validate force insert only when parents are inserted.
force_insert = self._validate_force_insert(force_insert)
parent_inserted = self._save_parents(
cls, using, update_fields, force_insert
)
updated = self._save_table(
raw,
cls,
force_insert or parent_inserted,
force_update,
using,
update_fields,
)
# Store the database on which the object was saved
self._state.db = using
# Once saved, this is no longer a to-be-added instance.
self._state.adding = False
# Signal that the save is complete
if not meta.auto_created:
post_save.send(
sender=origin,
instance=self,
created=(not updated),
update_fields=update_fields,
raw=raw,
using=using,
)
All done!