SQLAlchemy ORM
SQLAlchemy - стандартный ORM Python-бэкенда, и собес по нему быстро упирается в две вещи: понимаешь ли ты разделение Core/ORM и знаешь ли, что связи по умолчанию ленивы - источник N+1 и DetachedInstanceError. Эти грабли ловят в проде постоянно.
Типовые формулировки: «чем Core отличается от ORM?», «почему список объектов делает кучу запросов?», «что за DetachedInstanceError».
Core и ORM, связи через relationship
SQLAlchemy двухслоен. Core - SQL-выражения поверх таблиц, близко к самому SQL. ORM - классы, маппленные на таблицы, с сессией и навигацией по связям; он построен НА Core. Понимание слоёв помогает спускаться к Core, когда ORM мешает выразить сложный запрос.
relationship() даёт объектную навигацию (author.books) поверх внешнего ключа; сам FK (foreign key) задаёт ForeignKey в колонке, а back_populates держит обе стороны связи синхронными.
// relationship это удобство навигации, а не сам внешний ключ; без ForeignKey в схеме навигировать нечему.
- Core vs ORM
- SQL-выражения против классов, маппленных на таблицы
- relationship
- объектная навигация между связанными записями
Ленивая загрузка: N+1 и Detached
По умолчанию связь ленива: SQL к ней уходит при ПЕРВОМ обращении. В цикле по объектам это классический N+1 - обращение к author.books на каждой итерации даёт запрос на каждого автора. Лечат жадной загрузкой: joinedload тянет связь одним JOIN (хорош для «многие к одному»), selectinload - вторым SELECT ... IN (хорош для коллекций, не раздувает строки).
Осторожно с joinedload на большой коллекции: JOIN «один ко многим» декартово размножает строки родителя - для «многих» бери selectinload. И вторая беда лени: обращение к связи ПОСЛЕ закрытия сессии - DetachedInstanceError, грузить уже нечем. Лечения - eager-загрузка заранее или expire_on_commit=False.
// Правило выбора: «многие к одному» → joinedload (один JOIN), «один ко многим»/коллекция → selectinload (второй запрос без дублей).
# N+1:
for a in session.query(Author): # 1 запрос
a.books # +N запросов
# fix — коллекция через selectinload:
select(Author).options(selectinload(Author.books))- joinedload/selectinload
- жадные стратегии загрузки связей против N+1
- DetachedInstanceError
- доступ к ленивой связи вне живой сессии
Как отвечать: «Почему список объектов делает кучу запросов и как чинить?»
Это N+1 из-за ленивой загрузки связей. Первый запрос достаёт N объектов, а дальше на каждом при обращении к связи - author.books - SQLAlchemy шлёт отдельный запрос, итого N плюс один. В коде это незаметно, зато в логе SQL видно N однотипных селектов. Чиню жадной загрузкой заранее: для связи «многие к одному» - joinedload, он тянет одним JOIN; для коллекции «один ко многим» - selectinload, он делает второй SELECT ... IN и не раздувает строки декартовым произведением. joinedload на большой коллекции как раз и опасен этим раздуванием, поэтому там всегда selectinload.
Названа причина (ленивые связи), способ диагностики (лог SQL) и - главное - правильный выбор стратегии по типу связи с объяснением, почему joinedload плох на коллекциях.
На чём валят
- −Обращение к author.books в цикле при ленивой загрузке - запрос на каждого автора (N+1).
- −joinedload на большой коллекции размножает строки родителя (декартово) - для «многих» бери selectinload.
- −Вернуть объект со связями за пределы сессии и обратиться к ним - DetachedInstanceError.
- −Считать relationship внешним ключом: FK задаёт ForeignKey, relationship лишь навигирует.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Что задаёт
relationship()в модели SQLAlchemy?A)Правило каскадного удаления строк на стороне СУБД напрямуюB)Физический внешний ключ и ограничение на уровне базы данныхC)Навигацию между связанными объектами (author.books)D)Индекс по колонке для ускорения выборок по связипоказать ответ и разбор
+C)Навигацию между связанными объектами (author.books)// разбор: relationship() описывает объектную связь: через author.books SQLAlchemy подтянет связанные записи, а back_populates держит обе стороны синхронными. Физический внешний ключ задаёт ForeignKey на колонке; relationship — навигация поверх него на уровне объектов.
- По умолчанию связь
author.booksв SQLAlchemy загружается как?A)Лениво: отдельный запрос при первом обращенииB)К связям приходится обращаться только через сырой SQLC)Один раз при старте приложения и дальше берётся из кэшаD)Жадно: одним JOIN вместе с самим авторомпоказать ответ и разбор
+A)Лениво: отдельный запрос при первом обращении// разбор: По умолчанию relationship ленив: SQL к связанным записям уходит в момент обращения (author.books). Удобно, но в цикле по авторам даёт N+1. Жадные стратегии (joinedload, selectinload) грузят связь заранее и включаются явно в запросе.
- joinedload и selectinload — обе грузят связь заранее. В чём разница?A)joinedload делает один JOIN, selectinload — второй IN-запросB)selectinload работает исключительно с базой PostgreSQLC)Разницы нет, это устаревший синоним одной и той же стратегииD)joinedload только для чтения, а selectinload — для записи связей
показать ответ и разбор
+A)joinedload делает один JOIN, selectinload — второй IN-запрос// разбор: joinedload тянет связь тем же запросом через JOIN — хорошо для «многие к одному». selectinload делает второй запрос SELECT ... WHERE fk IN (...) — лучше для коллекций («один ко многим»), потому что JOIN на коллекции размножает строки родителя (декартово раздувание).
- Объект получен в сессии, сессия закрыта, потом обращаемся к
obj.books(ленивой связи). Что будет?A)Вернётся пустой список, раз данные не были загружены заранееB)Связь возьмётся из глобального кэша объектов приложенияC)DetachedInstanceError — связь грузить уже нечемD)SQLAlchemy откроет новую сессию автоматически и догрузит связьпоказать ответ и разбор
+C)DetachedInstanceError — связь грузить уже нечем// разбор: Ленивая связь грузится через сессию, к которой привязан объект. После закрытия/expunge объект detached, и обращение к незагруженной связи бросает DetachedInstanceError. Лечения: подгрузить заранее (eager), не выходить из сессии до использования, или expire_on_commit=False.
- Что делает ORM вроде SQLAlchemy?A)Отображает классы на таблицы, а объекты — на строки, пряча SQLB)Ускоряет запросы за счёт автоматического добавления индексов на все колонкиC)Переводит SQL с диалекта одной базы на диалект другой без изменения кода приложенияD)Заменяет базу данных, храня все объекты приложения прямо в оперативной памяти процесса
показать ответ и разбор
+A)Отображает классы на таблицы, а объекты — на строки, пряча SQL// разбор: ORM (Object-Relational Mapping) сопоставляет классы Python таблицам БД: атрибуты — колонкам, экземпляры — строкам, связи — внешним ключам. Вы работаете с объектами и методами, а ORM генерирует SQL. Это ускоряет разработку и уменьшает ручной SQL, но добавляет слой абстракции, который иногда полезно уметь обходить (сырой SQL для сложных запросов).
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.