By default in Django Admin, lost update or write skew caused by race condition can happen because select_for_update()
is not used. *My answer explains lost update and write skew.
So, I wrote the example code with select_for_update()
to prevent lost update or write skew in Django Admin as shown below. *I used Django 3.2.16 and PostgreSQL:
<Lost update>
For example, you create store_product
table with id
, name
and stock
with models.py
as shown below:
store_product
table:
id |
name |
stock |
1 |
Apple |
10 |
2 |
Orange |
20 |
# "store/models.py"
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=30)
stock = models.IntegerField()
Then, you need to override get_queryset() with select_for_update()
in ProductAdmin():
as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Product
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
last_part_of_referer = request.META.get('HTTP_REFERER').split('/')[-2]
last_part_of_uri = request.build_absolute_uri().split('/')[-2]
if (last_part_of_referer == "change" and last_part_of_uri == "change"):
qs = qs.select_for_update()
return qs
Then, if you change(update) product as shown below:

SELECT FOR UPDATE
and UPDATE
queries are run in transaction according to the PostgreSQL query logs as shown below. *You can check how to log PostgreSQL queries:

And, if you don't override get_queryset()
in ProductAdmin():
as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Product
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
pass
SELECT
and UPDATE
queries are run as shown below:

<Write skew>
For example, you create store_doctor
table with id
, name
and on_call
with models.py
as shown below:
store_doctor
table:
id |
name |
on_call |
1 |
John |
True |
2 |
Lisa |
True |
# "store/models.py"
from django.db import models
class Doctor(models.Model):
name = models.CharField(max_length=30)
on_call = models.BooleanField()
Then, you need to override response_change() with select_for_update()
and save_model() in DoctorAdmin():
as shown below. *At least one doctor must be on call:
# "store/admin.py"
from django.contrib import admin
from .models import Doctor
from django.db import connection
@admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
def response_change(self, request, obj):
qs = super().get_queryset(request).select_for_update().filter(on_call=True)
obj_length = len(qs)
if obj_length == 0:
obj.on_call = True
obj.save()
return super().response_change(request, obj)
def save_model(self, request, obj, form, change):
last_part_of_path = request.path.split('/')[-2]
if last_part_of_path == "add":
obj.save()
Then, if you change(update) doctor as shown below:

SELECT FOR UPDATE
and UPDATE
queries are run in transaction but I don't know how to remove the 1st SELECT
query in light blue as shown below:

And, if you don't override response_change()
and save_model()
in DoctorAdmin():
as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Doctor
@admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
pass
SELECT
and UPDATE
queries are run as shown below:

For example for write skew
again, you create store_event
table with id
, name
and user
with models.py
as shown below:
store_event
table:
id |
name |
user |
1 |
Make Sushi |
John |
2 |
Make Sushi |
Tom |
# "store/models.py"
from django.db import models
class Event(models.Model):
name = models.CharField(max_length=30)
user = models.CharField(max_length=30)
Then, you need to override response_add()
with select_for_update()
and save_model()
in EventAdmin():
as shown below. *Only 3 users can join the event "Make Sushi":
# "store/admin.py"
from django.contrib import admin
from .models import Event
from django.db import connection
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
def response_add(self, request, obj, post_url_continue=None):
qs = super().get_queryset(request).select_for_update() \
.filter(name="Make Sushi")
obj_length = len(qs)
if obj_length < 3:
obj.save()
return super().response_add(request, obj, post_url_continue)
def save_model(self, request, obj, form, change):
last_part_of_path = request.path.split('/')[-2]
if last_part_of_path == "change":
obj.save()
Then, if you add event as shown below:

SELECT FOR UPDATE
and INSERT
queries are run in transaction as shown below:

And, if you don't override response_add() and save_model()
in EventAdmin():
as shown below:
# "store/admin.py"
from django.contrib import admin
from .models import Event
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
pass
Only INSERT
query is run as shown below:

You can also see my posts below about SELECT FOR UPDATE in Django: