[Django] Race Condition & Transaction

DBMS’ ACID (Atomicity, Consistency, Isolation, Durability) and transaction in Django.

update Within One Step

If we need to update only one field of a model (That is, no other field or model need to be updated simultaneously to ensure the consistency of ACID), we can use F(), select_for_update() and filter() to update field without race condition..

Following example:

  • Check if the product adequate for n quantity before selling.
    • If adequate, sell n products.
    • If inadequate, sell nothing.
1
2
3
4
5
6
7
8
9
10
11
from django.db.models import F

class Product(models.Model):
quantity = models.IntegerField()
...

def sell_out(self, n):
Product.objects.select_for_update().filter(
pk = self.pk, # SELECT the model itself
quantity__gte = n,
).update(quantity=F('quantity') - n)
  1. select_for_update() returns a queryset that will lock rows until the end of the transaction (generating SELECT ... FOR UPDATE)
  2. FIELDNAME__gte = number means greater than or equal to number.
  3. filter() always return an objects set.
    When no object satisfied the query’s condition (quantity >= n), returns an empty set.
    An empty set won’t trigger the update() manipulation.

    • get() always return one object. Raise error when get more than one.
    • filter() always return an objects set. (obj, obj, obj, ...)
  4. F() object represents the value of a model field. It makes it possible to refer to model field values and perform database operations using them without actually having to pull them out of the database into Python memory.

Transaction With Multiple Steps

But we still need transaction with multiple steps. So use django.db.transaction.atomic() to ensure ACID’s atomic.

A transaction usually need to update multiple fields across models, so we implement the mechanism in views.py instead of models.py.

Now the following example implement the mechanism of add_to_cart():

  • User requests for n amount products via a form.
    • If quantity > requested amount n:
      • sell n products;
      • add n items into my Cart model.
    • If (quantity < requested amount n) && (quantity > 0):
      • sell out all stock of the quantity;
      • add sold items into my Cart model.
    • Else:
      • Sell nothing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from django.db import transaction

@require_http_methods(['POST', 'GET'])
def add_to_cart(request, pk):

buy_n = int(request.POST.get('quantity', 1))

with transaction.atomic():
product = Product.objects.get(pk=pk)

if buy_n <= product.quantity: # if quantity is adequate.
product.quantity = product.quantity - buy_n
elif product.quantity > 0: # quantity is inadequate, but > 0
buy_n = product.quantity
product.quantity = 0
product.save()
remain = int(Product.objects.get(pk=pk).quantity)

# Check if the same product exists in Cart.
_objects = CartItem.objects.filter(cart = request.user.cart,
product = product)
if len(_objects) == 0:
cart_item = CartItem.objects.create(cart = request.user.cart,
product = product,
quantity = buy_n)
else:
cart_item = _objects[0]
cart_item.quantity = cart_item.quantity + buy_n
cart_item.save()

if request.is_ajax():
return JsonResponse({'remain': remain,
'boughtAmount': buy_n,
'totalPriceAndShippingFee': request.user.cart.total_price_and_fee})
return redirect(reverse('product_detail', kwargs={'pk': pk}))