🐛 Типичные ошибки блока SQLAlchemy

⚡ Топ-3 ошибки блока

  1. Забыть session.commit() после add/delete.
  2. Использовать having() вместо filter() до группировки.
  3. Обращаться к атрибутам объекта после session.close() (DetachedInstanceError).

1. Забыть session.commit()

Добавление объекта через session.add() лишь переводит его в состояние Pending. Без commit() изменения не попадут в БД.

# Неправильно
session.add(User(name="Alice"))
# Изменения НЕ сохранены!

# Правильно
session.add(User(name="Alice"))
session.commit()

2. DetachedInstanceError после закрытия сессии

После session.close() объекты переходят в состояние Detached. Обращение к «ленивым» атрибутам (связи с lazy='select') вызовет ошибку.

# Неправильно
user = session.query(User).get(1)
session.close()
print(user.addresses)  # DetachedInstanceError!

# Правильно — загрузить данные до закрытия
user = session.query(User).get(1)
addresses = user.addresses  # загрузка в рамках сессии
session.close()
print(addresses)  # OK

3. having() вместо filter() до группировки

having() работает только с агрегатными функциями — после group_by(). Для фильтрации по обычным полям используйте filter().

# Неправильно
session.query(User).having(User.age > 25).all()
# SQLAlchemy может выполнить это, но это неправильная семантика

# Правильно
session.query(User).filter(User.age > 25).all()

# having() — только после group_by для агрегатов
session.query(User.age, func.count(User.id)).group_by(
    User.age
).having(func.count(User.id) > 1).all()

4. Забыть Base.metadata.create_all(engine)

Без вызова create_all() таблицы в базе данных не создадутся, и любые запросы вернут ошибку.

# Обязательно вызвать после определения моделей
Base.metadata.create_all(engine)

5. Путать .one() и .first()

.one() выбрасывает исключение при 0 или 2+ результатах. .first() просто возвращает None при отсутствии результатов. Выбирайте в зависимости от ожиданий.

# .one() — ожидается ровно один результат
user = session.query(User).filter(User.email == "alice@example.com").one()
# MultipleResultsFound или NoResultFound при несоответствии

# .first() — ожидается ноль или один результат
user = session.query(User).filter(User.name == "Alice").first()
# None если не найден

6. Импорт declarative_base из неверного модуля

В SQLAlchemy 1.x модуль был sqlalchemy.ext.declarative, в 2.x переехал в sqlalchemy.orm.

# Из лекции (устарело, но совместимо)
from sqlalchemy.ext.declarative import declarative_base

# Правильно для актуальных версий
from sqlalchemy.orm import declarative_base

7. Не закрыть соединение при ошибке

При ошибке в транзакции нужно делать rollback(), иначе соединение окажется в неопределённом состоянии.

try:
    session.add(User(name="Alice"))
    session.commit()
except Exception as e:
    session.rollback()
    raise
finally:
    session.close()

8. Двойная инициализация relationship без back_populates

При создании двунаправленной связи без back_populates (или backref) SQLAlchemy не знает о второй стороне связи, что приводит к неожиданному поведению.

# Неправильно (только одна сторона)
class User(Base):
    addresses = relationship("Address")
class Address(Base):
    user = relationship("User")  # независимая связь, не синхронизирована

# Правильно
class User(Base):
    addresses = relationship("Address", back_populates="user")
class Address(Base):
    user = relationship("User", back_populates="addresses")