Table of Contents
When should you lock database records?
You've got a Django project, and you're writing a piece of code to fetch a record and then change it afterwards, before saving the changes back into the database.
If you've ever written a custom 'update or create' function, you'll have implemented this pattern before.
The key aspect to consider is the selection part. Let's say that you have two possible ways to try to fetch an existing record. You might run some code like this:
# Try fetch object based on ID
obj = MyModel.objects.filter(id=1).first()
# If no object found, try to select it by slug
if not obj:
obj = MyModel.objects.filter(slug='some-slug').first()
if obj:
# change some value
obj.foo = 'bar'
obj.save(['foo'])
The issue with this approach is that in between the selection and the update/save, another piece of code could potentially make changes to the model you retrieved.
Because my above example is very simple, the code should run and finish almost immediately, meaning that it's very unlikely to encounter this problem. But, for argument's sake, let's say we're updating a lot of fields, and the database we're using is very large, making the queries slower.
It's now very possible that in the time between us selecting the record, altering and then saving it, another piece of code could have amended that same record, causing a conflict and potential loss of data from one of the two processes. That's where locking the record comes into play.
What does it mean to 'lock' the record?
To 'lock the record' means to tell the database that this particular piece of data is currently in use and shouldn't be given to anyone else who requests it. We're telling the database that "until I'm done, nobody else can use this information".
Once your process is complete, the database is told that the record is free to be used again, and it can serve the information to the next process that needs it.
How to lock down a record
For the sake of accuracy, I'm going to provide a code snippet taken directly from a project I've been working on. But, firstly, some context:
This is a QuerySet subclass with a new method called update_or_create_from_remote_taxonomy. This method is given a remote_taxonomy object (a dataclass with some information attached), and this method should then find the appropriate record in the database to match this object, and then should update the record in the database to match the object's data. This functionality is all part of a larger system which is regularly importing data from a remote source and keeping our copy up-to-date.
from django.db import models, transaction
class TaxonomyQuerySet(models.QuerySet):
def update_or_create_from_remote_taxonomy(
self, remote_taxonomy
) -> tuple["Taxonomy", bool]:
remote_id = (remote_taxonomy.remote_id or "").strip() or None
# atomic transaction ensures that the record is locked until the end of the block
with transaction.atomic():
taxonomy_obj = None
if remote_id:
taxonomy_obj = (
self.select_for_update().filter(remote_id=remote_id).first()
)
if not taxonomy_obj:
taxonomy_obj = (
self.select_for_update().filter(slug=remote_taxonomy.slug).first()
)
if taxonomy_obj:
data = remote_taxonomy.updating_data()
for key, value in data.items():
setattr(taxonomy_obj, key, value)
taxonomy_obj.save(update_fields=list(data.keys()))
return taxonomy_obj, False
taxonomy_obj = self.create(**remote_taxonomy.creating_data())
return taxonomy_obj, True
You can see on line 9 that I've added a comment to show where the magic happens. Using transaction.atomic, you can start a with block. Within that block, any records that you fetch using select_for_update will automatically flag in the database as being locked. Once the with block ends, these records are automatically released.
From line 10, you can see that we're trying to select a record based on two criteria, locking the retrieved record (if any) using select_for_update. If we find an object (line 21), we then loop through some data to update the record and save it back to the database. If we don't find any records (line 28), we just create a new one.
Locking records in Wagtail CMS
This same approach can be used in Wagtail to lock records. As Wagtail is built with Django, the process is identical. However, this only applies to standard Django models, and not Wagtail's page models. Wagtail's pages have their own system for locking records between edits. So if you're using Wagtail, you can follow this guide like normal for any Django models, such as what you'd use in snippets or taxonomies. But you shouldn't apply this to instances of page models.
FAQs
How do you lock a record in a Django database?
Using Model.objects.select_for_update, you can retrieve a record from the database, locking it from being edited by other processes. Using this in combination with django.db.transaction.atomic, you can efficiently lock and release records to preserve data integrity.
What happens if another process tries to reteive a locked record?
If another process tries to use a record that you have locked, it'll simply hang until the record is free. This has a certain risk that a request could timeout if the record isn't freed up within a certain window. However, you should be intentional with how and when you lock your records to ensure this doesn't happen. In general, locking records shouldn't result in any errors, as processes will naturally hang until they're served the data they need.
Why should you lock records in Django?
If you're fetching and then updating records in separate transactions, there is the possibility that another process could also select/update that record in the database in the meantime. If these two processes then make changes and save them back to the database, their work could be overwritten by the other, resulting in unexpected data loss. Locking records ensures better reliability and data integrity.