Почему не срабатывает каскадное удаление в SQLAlchemy
При работе с базами данных в Python через SQLAlchemy часто возникает ситуация, когда удаление родительской записи не приводит к автоматическому удалению дочерних записей. Рассмотрим типичный пример с моделями Order и ItemInBasket.
Проблема: дочерние записи остаются после удаления родителя
В коде используется прямой SQL-запрос delete(Order).where(Order.tg_id == tg_id). Такой подход выполняет удаление на уровне базы данных, минуя ORM-механизмы SQLAlchemy. В результате каскадное удаление, настроенное через ForeignKey('orders.id', ondelete='CASCADE'), не срабатывает.
Причина: ORM vs Core
SQLAlchemy предоставляет два уровня работы: Core (низкоуровневый) и ORM (объектно-реляционное отображение). При использовании session.execute(delete(...)) вы работаете через Core, который не учитывает ORM-правила, включая каскады, заданные в моделях. Каскадное удаление в данном случае - это декларация на уровне ORM, а не на уровне базы данных.
Решение 1: Использовать сессию и ORM-удаление
Правильный способ - получить объект через ORM и удалить его с помощью session.delete(). SQLAlchemy автоматически обработает каскады:
async def create_order(user_name: str, tg_id: int):
async with async_session() as session:
# Получаем объект заказа
order = await session.get(Order, tg_id)
if order:
await session.delete(order) # ORM удалит и связанные ItemInBasket
await session.commit()
# Создаём новый заказ
session.add(Order(user_name=user_name, tg_id=tg_id))
await session.commit()Решение 2: Настроить каскад на уровне базы данных
Если нужно оставить прямой SQL-запрос, следует явно удалить дочерние записи перед удалением родителя:
async def create_order(user_name: str, tg_id: int):
async with async_session() as session:
# Сначала удаляем связанные ItemInBasket
await session.execute(delete(ItemInBasket).where(ItemInBasket.order_id == tg_id))
# Затем удаляем Order
await session.execute(delete(Order).where(Order.tg_id == tg_id))
await session.commit()
# Создаём новый заказ
session.add(Order(user_name=user_name, tg_id=tg_id))
await session.commit()Решение 3: Использовать cascade='all, delete' в relationship
Добавьте в модель Order relationship с правильным каскадом, чтобы ORM понимал, что нужно удалять связанные объекты:
class Order(Base):
__tablename__ = 'orders'
id: Mapped[int] = mapped_column(primary_key=True)
user_name: Mapped[str] = mapped_column(String(256))
tg_id: Mapped[BigInteger] = mapped_column(BigInteger)
check_image: Mapped[str] = mapped_column(String(1024), nullable=True)
created_at: Mapped[str] = mapped_column(String(128), nullable=True)
items = relationship('ItemInBasket', cascade='all, delete', backref='order')Теперь при удалении объекта Order через session.delete() все связанные ItemInBasket будут удалены автоматически.
Вывод
Ключевое правило: если вы хотите использовать ORM-возможности SQLAlchemy (каскады, relationship), удаляйте объекты через session.delete(), а не через прямые SQL-запросы. В противном случае настройки каскадов в моделях игнорируются, и дочерние записи остаются в базе.