Совет дня №13
Работая с ORM, избегайте проблемы 1 + N. Это когда вы обращаетесь к ссылочным полям, и база подтягивает сущности штучно, а не разом.
Пример: магазин товаров, сущность Order ссылается на User (кто заказал) и
Item (что заказали). Модель выглядит так:
class Order(model.Model):
status = EnunField(active, cancelled, pending...)
created_at = DateTimeFiled(now=True)
user = ForeignField(class=User)
item = ForeignField(class=Item)
Типичная задача — вывести активные заказы с информацией о клиенте и товаре. Разработчик делает так:
orders = Orders.filter(status=active) \
.order_by(created_at, desc).all()
Затем он строит таблицу:
for order in orders:
print order.id, order.user.name, order.item.title
Что произойдет под капотом? Сначала выполнится запрос:
select * from orders where status = 'active'
order by created_at desc;
Тут все в порядке. Однако в цикле, когда происходит обращение к полям user и
item, выполняются запросы get-by-id:
select * from users where id = 1
select * from users where id = 2
...
select * from items where id = 100
select * from items where id = 200
...
В среднем запросов будет 1 + 2N. На практике в одном заказе может быть много товаров, а кроме того, возможны подгрузки других сущностей. Скажем, товар хранит ссылку на продавца. Если в таблице должен быть продавец, это будет еще +N запросов.
Именно для таких случаев нужны прошлые советы. Во-первых, запросы должны быть видны в консоли, и разработчик обязан смотреть, что идет в базу. Во-вторых, на эту логику должен быть тест, который считает запросы.
Проблема 1 + N лечится разными способами. Первый — ORM может джойнить сущности, то есть выполнить запрос:
select * from orders
left join users on orders.user_id = user.id
left join items on order.item_id = item.id
where status = 'active'
order by created_at desc;
Такой джоин может нарушить пагинацию по limit/offset, но это страшно. Можно либо не использовать ее вообще, либо взять пагинацию по keyset, либо заменить limit выражением fetch.
Другой способ — вытянуть записи по слоям на уровне приложения. Первый слой — это orders. Как только происходит обращение к user, ORM собирает все user_id и выполнят запрос
select * from users where id in (?, ?, ? ...)
То же самое с items — выгребаются уникальные item_id, и по ним делается
запрос с IN.
Некоторые ORM действуют тоньше. Если айдишников много, они выгребают смежные сущности кусками по 10-30. Таким образом, если нужно пройти 100 заказов, мы совершим 4-10 запросов, что не так страшно.
Проблема 1 + N — настоящий бич ORM. Сколько подобных ошибок я исправил — затрудняюсь припомнить (и конечно, совершил сам).
На мой взгляд, в ORM должна быть опция: кидать исключение, если смежные записи читаются штучно. Опция должна быть глобальной, чтобы раз и навсегда запретить подобные вещи. Глядишь, новички стали бы лучше понимать, что вообще происходит.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter