Adding fields to database schemas to audit the creation and modification is a common best practice, useful for any number of things, most commonly debugging and cache invalidation. The good news is, Django has field-types built for just this purpose!
auto_now and auto_now_add fields
As always, let’s start with the docs!
These fields are built into Django for expressly this purpose - auto_now fields are updated to the current timestamp every time an object is saved and are therefore perfect for tracking when an object was last modified, while an auto_now_add field is saved as the current timestamp when a row is first added to the database, and is therefore perfect for tracking when it was created.
It’s worth noting that that both fields are set on initial creation, whether they go through .save() or bulk_create - they may be a few milliseconds different, but will be effectively the same, but an auto_now_add field won’t change again after it’s set.
Let’s dive in and talk through some quirks of these fields.
Read-only fields
Django’s DateField definition includes this on init:
if auto_now or auto_now_add:
kwargs['editable'] = False
kwargs['blank'] = True
super(DateField, self).__init__(verbose_name, name, **kwargs)
This has some implications for both your Django admin set-up, as well as different-than-usual options, should you need to update these yourself.
Django Admin
Because both of these fields are read only, by default they won’t show up in your django admin view. If you try to explicitly include them via the fields option, you’ll see an error that looks like this:
'created_at' cannot be specified for <ModelClass> model form as it is a non-editable field
If you want them to appear anyway, you can add them to readonly_fields
on a ModelAdmin
class, and they will be displayed on the form in a non-editable way. They can also be included in list_display
.
Manually changing the values
Because an auto_now_add
field is only set on initial creation, not on can be changed manually in the same way you’d update any other field - by setting the value and calling .save()
.
However, if you were to do this with an auto_now
field, because calling .save()
would itself change the value, the value you manually set would not be reflected. But fear not - there are a couple ways you can update it!
Because the function of an auto_now_add field relies on Django’s .save()
mechanism, any value can be changed using a SQL update statement
You can also do this via .update()
- for the same reason calling .update()
does not update the value even if you might want it to, we can take advantage of this fact and pass a value to the statement, and it will be persisted. You can always do this for a single row with a statement like <Model>.objects.filter(id=<object_id>).update(modified_at=<desired timestamp>)
Unintended Consequences - User-facing values & backfills
Customers often want to know when something was created or updated - this can be useful information for sorting, display, etc. However, these fields may not be the right choice for displaying this information to users. In general, I’d recommend using them only for internal auditing purposes.
A customer imports data and wants the reflected date of creation to be when the resource was originally created, not when it was created within your system
You as a developer need to backfill some data, which results in an update of every row in the table - customers might be dismayed to find that all of their resources were updated at the same time now!
For reasons like these, I recommend having separate customer-facing fields that only update on customer actions, and only using these built-in Django fields for internal auditing purposes.
Bonus: automatically adding this to all your models!
If you know you want these auditing fields added to all of your models, and you don’t want to have to remember to add them each time you create a new model, you can create a base class that looks something like this:
class YourBaseClass(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
And then instead of your model classes being instances of models.Model, they can use YourBaseClass instead.
Top comments (0)