Один из способов сделать это - полагаться на проверки целостности базы данных и транзакции.Предполагая, что ваша емкость всегда должна быть в диапазоне [0, + бесконечность), вы можете изменить модель Coupon
на PositiveIntegerField
вместо IntegerField
:
class Coupon(models.Model):
name = models.CharField(max_length=255)
capacity = models.PositiveIntegerField()
Затем необходимо обновитьваша Coupon
емкость каждый раз, когда создается CouponUsage
.Вы можете переопределить метод save()
, чтобы отразить это изменение:
from django.db import models, transaction
class CouponUsage(models.Model):
coupon = models.ForeignKey('Coupon', on_delete=models.CASCADE, related_name="usage")
date = models.DateTimeField(auto_now_add=True)
@transaction.atomic()
def save(self, ...): # Arguments missing
if not self.pk: # This is an insert, you may want to raise an error otherwise
self.coupon.capacity = models.F('capacity') - 1 # The magic is here, this is executed at the database level so no problem with old in memory values
self.coupon.save()
super().save(...)
Теперь, когда создается CuponUsage
, вы обновляете емкость для связанного экземпляра Coupon
.Ключевым моментом здесь является то, что вместо чтения значения из базы данных в память Python, обновления и сохранения, что может привести к противоречивым результатам, обновление до capacity
выполняется на уровне базы данных с использованием выражения F ,Это гарантирует, что никакие две транзакции не будут использовать одно и то же значение.
Теперь обратите внимание, что при использовании поля PositiveInteger
вместо IntegerField
база данных также гарантирует, что capacity
не может упасть ниже 0. Поэтому еслиТеперь вы пытаетесь создать экземпляр CuponUsage
таким образом, чтобы емкость Cupon
получала отрицательное значение, возникло исключение, что препятствовало созданию такого CuponUsage
.
Теперь вам нужно воспользоватьсяоб этом в вашем коде, выполнив что-то вроде следующего:
def use_coupon(request):
coupon = Coupon.objects.get(condition)
try:
usage = CuponUsage.objects.create(coupon=coupon)
# Do whatever you want here, you already 'consumed' a coupon
except IntegrityError: # Check for the specific exception
# Sorry no capacity left
pass
Если в случае получения купона вам нужно сделать что-то, что может потерпеть неудачу, и в таком случае вам нужно «вернуть»Использование, вы можете заключить всю вашу функцию use_coupon
внутри транзакции.