Почему не срабатывает каскадное удаление в 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-запросы. В противном случае настройки каскадов в моделях игнорируются, и дочерние записи остаются в базе.

    Часто задаваемые вопросы