🐛 Типичные ошибки блока 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.get(User, 1)
session.close()
print(user.addresses)  # DetachedInstanceError!

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

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

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

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

# Правильно
session.scalars(select(User).where(User.age > 25)).all()

# having() — только после group_by для агрегатов
session.execute(
    select(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.scalars(
    select(User).where(User.email == "alice@example.com")
).one()
# MultipleResultsFound или NoResultFound при несоответствии

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

6. Забыть базовый класс DeclarativeBase

ORM-модель должна наследоваться от общего Base. Иначе SQLAlchemy не добавит её таблицу в Base.metadata.

# Неправильно
class User:
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)

# Правильно
class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)

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

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

with Session(engine) as session:
    try:
        session.add(User(name="Alice"))
        session.commit()
    except Exception:
        session.rollback()
        raise

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")