QuerySet.update() возвращает число изменённых строк, а не User. Исходный код уже списывает деньги до проверки, затем пытается обращаться к balance у числа; проверку и списание нужно объединить атомарно.
Django update: число строк вместо User и списание баланса
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 предотвращает перерасход при конкуренции. Реальная платёжная операция также требует журнала и идемпотентности: этот фрагмент сам по себе не защищает от повторной отправки одного списания.