Почему отложенная загрузка данных плохо работает в асинхронном варианте
Что проверяют
Понимаете ли вы, что ленивая подгрузка это скрытый поход в базу внутри обращения к атрибуту, и почему такое обращение невозможно сделать асинхронным незаметно для вызывающего кода. Вопрос быстро отделяет тех, кто писал асинхронный доступ к данным, от тех, кто читал про него.
Как отвечать
На примере SQLAlchemy. В синхронном варианте order.items при первом обращении сам выполняет запрос прямо внутри чтения свойства. В асинхронном так нельзя: await не вставить в обычное выражение, поэтому обращение к незагруженной связи не ходит тихо в базу, а падает с ошибкой про отсутствующий гринлет.
Что с этим делают:
- грузят заранее, прямо в запросе:
selectinloadилиjoinedload; - ставят на связь
lazy="raise", чтобы забытая предзагрузка падала на тестах, а не в проде; - выставляют
expire_on_commit=False, иначе после коммита атрибуты протухают и следующее чтение снова захочет сходить в базу; - если ленивость нужна точечно, зовут её явно через
awaitable_attrs.
Второй слой ответа про цену: даже там, где ленивость работает, она даёт проблему N плюс один запрос, а в конкурентной обработке это N плюс один на каждую корутину, и пул подключений выедается мгновенно. Асинхронность не ускоряет запросы, она лишь позволяет запустить их больше одновременно.
Где спотыкаются
Объясняют проблему гонками. Гонки тут ни при чём: сессия вообще не рассчитана на одновременное использование из нескольких задач, и одна сессия на несколько корутин ломается раньше и по другой причине.
Лечат симптом обёрткой: уносят синхронный вызов в отдельный поток. Запрос действительно перестанет мешать циклу событий, но объект останется привязан к своей сессии, а число походов в базу не изменится.
Считают, что предзагрузка решает всё. Она превращает N запросов в два, но на большой выборке второй собирает огромный список идентификаторов, поэтому глубину связей и размер страницы всё равно приходится ограничивать.