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, выполнять работу.
- Избежать состояния гонки, когда два процесса получают и обновляют один и тот же экземпляр.