Набор запросов для фильтрации ровно N объектов из таблицы

Я пытаюсь понять, как выделить ровно N доступных объектов таким образом, чтобы это было безопасно для одновременной работы. Есть ли способ написать кверисет, который не сможет вернуть хотя бы N объектов?

Это, вероятно, излишняя осторожность для моего конкретного приложения, поскольку шансы на гонку ничтожно малы, но мне интересно

То, что я знаю, не сработает

available = Foo.objects.filter( status=Foo.AVAILABLE).count()
if available < N:
    #let the user know there aren't enough left

# but now something concurrent may grab them! 

foo_qs = Foo.objects.select_for_update().filter(status=Foo.AVAILABLE)[:N]
with transaction.atomic():
    for foo in foo_qs:
        ... # quite a bit. In RL I will  have locked related objects as well.
        foo.status=Foo.RESERVED
        foo.save()

потому что нарезка кверисета гарантирует только не более N объектов.

Удаление среза может заблокировать большое количество строк, которые мне не нужны. Является ли это неэффективным? Вся таблица Foo не будет заблокирована надолго, потому что я обновляю только N объектов и затем выхожу из транзакции.

Является ли единственным решением захват объектов по одному за раз внутри транзакции, или получение всех объектов в виде списка и повторная проверка его длины?

with transaction.atomic():
    foos = list( foo_qs)
    if len(foos) < N:
        raise ...    # fail transaction
    for foo in foos:
        ... # as before

Может быть, что-то вроде:

with transaction.atomic():
   # Locks before checking the count
   available = Foo.objects.select_for_update().filter(status=Foo.AVAILABLE)[:N]

   if available.count() >= N:
      available.update(status=Foo.RESERVED)
   else:
      raise ...    # fail transaction

Используя list:

with transaction.atomic():
   # Locks before checking the length
   available = list(Foo.objects.select_for_update().filter(status=Foo.AVAILABLE)[:N])

   if len(available) >= N:
      for foo in available:
         foo.status=Foo.RESERVED
         foo.save()
   else:
      raise ...    # fail transaction
Вернуться на верх