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: