Обновление запросов в django с помощью выражения F()


TL; DR

Когда вы обращаетесь к полю модели для операций чтения/записи, давайте использовать выражение F().

  • Помогает ссылаться на поле модели непосредственно в базе данных, не нужно загружать его в память Python -> экономит запросы.
  • Это поможет избежать состояния гонки или грязного чтения.
  • Необходимо обновлять_from_db после запроса, так как Python знает только SQL-выражение, а не фактический результат.

Массовое обновление

Предположим, правительство вашей страны повысило налоговую ставку на 5%, что вынуждает вас поднять цену вашего товара на 20%. Как будет выглядеть ваш запрос на django?

class Product(models.Model):

    name = models.TextField()
    price = models.DecimalField()
    in_stock = models.IntegerField(
        help_text="Number of items available in inventory"
    )
Вход в полноэкранный режим Выйти из полноэкранного режима

Наивная реализация обновления нескольких продуктов может выглядеть следующим образом:

products = Product.objects.all()
for product in products:
    product.price *= 1.2
    product.save()
Вход в полноэкранный режим Выход из полноэкранного режима

В этом случае вы делаете SELECT price FROM product затем UPDATE product SET price = new_value WHERE condition каждую запись. Это означает 2 запроса (1 для READ и 1 для WRITE) для каждого объекта.

Подумайте об этом более тщательно, мы можем понять, что новая цена является относительной по отношению к текущей цене, независимо от того, какая она. Интуитивно понятно, что мы хотим ссылаться на поле price модели Product при выполнении процесса обновления.

И вот оно, выражение F(). В официальной документации Django говорится:

Объект F() представляет значение поля модели, преобразованное значение поля модели или аннотированный столбец.
Он позволяет ссылаться на значения полей модели и выполнять операции с базой данных, используя их без необходимости извлекать их из базы данных в память Python.

Попробуем решить задачу с помощью метода F() и update() queryset

from django.db.models import F

Product.objects.update(price=F("price")*1.2)
Войти в полноэкранный режим Выйти из полноэкранного режима

Хотя приведенный выше запрос выглядит как обычное присвоение значения атрибуту экземпляра в Python, на самом деле это выражение SQL. Это выражение инструктирует базу данных умножить поле цены в базе данных на 120 процентов.

Новое значение цены основано на текущем значении цены, поэтому нам не нужно загружать его в память Python. Именно поэтому в дело вступает F().

Обновление одного объекта

Допустим, вы хотите обновлять поле in_stock после каждой оплаты заказа.

Наивная реализация может выглядеть следующим образом:

def process_payment(product: Product):
    with transaction.atomic():
        payment = Payment.objects.create(product=product)
        product.in_stock = product.in_stock - 1
        product.save(update_fields=["in_stock"])
Войти в полноэкранный режим Выход из полноэкранного режима

Так в чем же проблема?
Представим, что несколько пользователей пытаются сделать заказ на товар, сценарий выглядит следующим образом:

Процесс 1 Процесс 2 на_складе
Выберите in_stock -> 5 5
Выберите in_stock -> 5 5
Обновление in_stock = 5-1 4
Обновить in_stock = 5-1 4

В этом случае два процесса обновляют product.in_stock одновременно, но значение in_stock просто уменьшается на 1. Это неправильно.

Основная проблема в том, что вы уменьшаете in_stock на основе того, что вы извлекли, а что если вы дадите базе данных команду обновить in_stock на основе того, что хранится в данный момент?

def process_payment(product: Product):
    with transaction.atomic():
        payment = Payment.objects.create(product=product)
        product.in_stock = F("in_stock") - 1
        product.save(update_fields=["in_stock"]])
Вход в полноэкранный режим Выйти из полноэкранного режима

Разница между двумя подходами довольно мала, но давайте посмотрим на SQL, генерируемый командами обновления:

Наивный подход:

UPDATE product_product
SET in_stock = 4
WHERE id = 262;
Войти в полноэкранный режим Выйти из полноэкранного режима

Это уменьшит количество = 4 независимо от текущего значения in_stock в базе данных

Подход F():

UPDATE product_product
SET in_stock = in_stock + 1
WHERE id = 262;
Войти в полноэкранный режим Выход из полноэкранного режима

Количество товара с id 262 уменьшится на 1, а не установится на фиксированное значение. Вот как использовать выражение F для решения проблемы состояния гонки.

Примечание

Объект F(), который присваивается полю модели, сохраняется после сохранения экземпляра модели и будет применяться при каждом save(), поэтому нам нужно refresh_from_db, чтобы получить обновленный экземпляр.

Попытка прочитать экземпляр без обновления из базы данных может привести к неожиданному результату:

In [12]: product = Product.objects.get(id=262)

In [13]: product.in_stock = F("in_stock") - 1

In [14]: product.save()

In [15]: product.in_stock
Out[15]: <CombinedExpression: F(in_stock) - Value(1)>

In [16]: 
Вход в полноэкранный режим Выход из полноэкранного режима

Резюме

На протяжении всей статьи мы указывали на два случая использования выражения F()

  • Сократить количество запросов при выполнении некоторых операций, заставляя базу данных, а не Python, выполнять работу.
  • Избежать состояния гонки, когда два процесса получают и обновляют один и тот же экземпляр.

Оцените статью
devanswers.ru
Добавить комментарий