2

In Django views, we can use select_for_update() to prevent race condition(lost update or write skew) so race condition doesn't happen in Django views with select_for_update(). *I used Django 3.2.16.

But, even though I googled, I couldn't find any information saying "in Django admin, race condition doesn't happen or select_for_update() is used to prevent race condition".

So, in Django admin, does race condition happen?

  • If yes, are there any ways to prevent race condition in Django admin?

  • If no, is select_for_update() or other way used to prevent race condition in Django admin? and can I see the code for me?

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129

2 Answers2

1

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:

enter image description here

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:

enter image description here

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:

enter image description here

<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:

enter image description here

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:

enter image description here

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:

enter image description here

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:

enter image description here

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

enter image description here

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:

enter image description here

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

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
0

Yes, a race condition can occur in Django admin if multiple users try to update the same object at the same time. This can result in lost updates or write skew, where the final value of the object is not what was intended. To avoid this, Django admin uses optimistic locking to ensure that updates are performed safely and consistently. This means that if two users try to update the same object simultaneously, one of the updates will be rejected and the user will need to retrieve the latest version of the object and try again.

Yug Damor
  • 11
  • 1
  • 1
  • 1
    I couldn't find such information `Django admin uses optimistic locking to ensure that updates are performed safely and consistently` in Django documentation. Are there such information in Django documentation? – Super Kai - Kazuya Ito Dec 10 '22 at 18:36