Django update: число строк вместо User и списание баланса

QuerySet.update() возвращает число изменённых строк, а не User. Исходный код уже списывает деньги до проверки, затем пытается обращаться к balance у числа; проверку и списание нужно объединить атомарно.

Короткий ответ

Что ответить на собеседовании

QuerySet.update() возвращает число изменённых строк, а не User. Исходный код уже списывает деньги до проверки, затем пытается обращаться к balance у числа; проверку и списание нужно объединить атомарно.

Подробный разбор

Ответ с пояснениями

Условие

def withdraw(user_id, amount):

user = User.objects.filter(user_id=user_id).update(balance=F('balance') - amount)
if user.balance > amount:
    # raise InsufficientFundsError()
    user.balance -= amount
    user.save()

Ответ

QuerySet.update() возвращает число изменённых строк, а не User. Исходный код уже списывает деньги до проверки, затем пытается обращаться к balance у числа; проверку и списание нужно объединить атомарно.

Для положительной целой суммы в минимальных денежных единицах и уникального user_id:

from django.db.models import F

class InsufficientFundsError(Exception):
    pass

def withdraw(user_id, amount):
    if type(amount) is not int or amount <= 0:
        raise ValueError('amount must be a positive integer')
    updated = User.objects.filter(
        user_id=user_id, balance__gte=amount,
    ).update(balance=F('balance') - amount)
    if updated != 1:
        raise InsufficientFundsError('Account missing or insufficient funds')

Условный UPDATE предотвращает перерасход при конкуренции. Реальная платёжная операция также требует журнала и идемпотентности: этот фрагмент сам по себе не защищает от повторной отправки одного списания.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее