SQLAlchemy: каскадное удаление

116

Должно быть, мне не хватает чего-то тривиального с каскадными параметрами SQLAlchemy, потому что я не могу заставить простое каскадное удаление работать правильно - если родительский элемент удален, дочерние элементы сохраняются с nullвнешними ключами.

Я поместил здесь краткий тестовый пример:

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key = True)

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key = True)
    parentid = Column(Integer, ForeignKey(Parent.id))
    parent = relationship(Parent, cascade = "all,delete", backref = "children")

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

session = Session()

parent = Parent()
parent.children.append(Child())
parent.children.append(Child())
parent.children.append(Child())

session.add(parent)
session.commit()

print "Before delete, children = {0}".format(session.query(Child).count())
print "Before delete, parent = {0}".format(session.query(Parent).count())

session.delete(parent)
session.commit()

print "After delete, children = {0}".format(session.query(Child).count())
print "After delete parent = {0}".format(session.query(Parent).count())

session.close()

Вывод:

Before delete, children = 3
Before delete, parent = 1
After delete, children = 3
After delete parent = 0

Между родительским и дочерним объектами существует простая связь «один ко многим». Скрипт создает родителя, добавляет 3 потомка, а затем фиксирует. Затем он удаляет родителя, но потомки остаются. Зачем? Как заставить детей каскадно удалять?

деревенщина
источник
Этот раздел в документации (по крайней мере, сейчас, спустя 3 года после исходного сообщения) кажется весьма полезным в этом отношении: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Ответы:

185

Проблема в том, что sqlalchemy считается Childродительским, потому что именно там вы определили свои отношения (конечно, неважно, что вы назвали его «Дочерним»).

Если Parentвместо этого вы определите отношения в классе, это будет работать:

children = relationship("Child", cascade="all,delete", backref="parent")

(обратите внимание "Child"на строку: это разрешено при использовании декларативного стиля, чтобы вы могли ссылаться на класс, который еще не определен)

Вы также можете добавить delete-orphan( deleteприводит к удалению дочерних элементов при удалении родителя, delete-orphanтакже удаляет всех дочерних элементов, которые были «удалены» от родителя, даже если родитель не удален)

РЕДАКТИРОВАТЬ: только что выяснилось: если вы действительно хотите определить отношения в Childклассе, вы можете это сделать, но вам нужно будет определить каскад на обратной ссылке (путем создания обратной ссылки явно), например:

parent = relationship(Parent, backref=backref("children", cascade="all,delete"))

(подразумевая from sqlalchemy.orm import backref)

Стивен
источник
6
Ага, вот и все. Хотелось бы, чтобы в документации было более подробно об этом!
Карл
15
Да. Очень полезно. У меня всегда были проблемы с документацией SQLAlchemy.
ayaz
1
Это хорошо объяснено в текущем документе docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc,
1
@Lyman Zerga: в примере OP: если вы удаляете Childобъект из parent.children, должен ли этот объект быть удален из базы данных или должна быть удалена только его ссылка на родительский элемент (т.е. установить для parentidстолбца значение null вместо удаления строки)
Стивен
1
Подождите, relationshipэто не диктует настройку родитель-потомок. Использование ForeignKeyна столе - вот что настраивает его как ребенка. Не имеет значения, принадлежит ли relationshipон родителю или ребенку.
d512 04
110

@ Ответ Стивена хорош, когда вы удаляете через, session.delete()что никогда не происходит в моем случае. Я заметил, что большую часть времени я удаляю через session.query().filter().delete()(что не помещает элементы в память и удаляет непосредственно из db). Использование этого метода sqlalchemy cascade='all, delete'не работает. Однако есть решение: ON DELETE CASCADEчерез db (примечание: не все базы данных поддерживают его).

class Child(Base):
    __tablename__ = "children"

    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parents.id", ondelete='CASCADE'))

class Parent(Base):
    __tablename__ = "parents"

    id = Column(Integer, primary_key=True)
    child = relationship(Child, backref="parent", passive_deletes=True)
Алексей Окрушко
источник
3
Спасибо за объяснение этой разницы - я пытался использовать session.query().filter().delete()и изо всех сил пытался найти проблему
nighthawk454
4
Мне пришлось установить passive_deletes='all', чтобы дочерние элементы были удалены каскадом базы данных при удалении родителя. При этом passive_deletes=Trueдочерние объекты отделялись (родительский объект был установлен на NULL) до того, как родительский объект был удален, поэтому каскад базы данных ничего не делал.
Милорад Поп-Тошич
@ MiloradPop-Tosic Я не использовал SQLAlchemy более 3 лет, но чтение документа выглядит как passive_deletes = True по-прежнему правильное решение.
Алекс Окрушко 08
2
Я могу подтвердить, что passive_deletes=Trueв этом сценарии все работает правильно.
d512 04
У меня были проблемы с автоматическим созданием ревизий алембика, которые включали каскад при удалении - это был ответ.
JNW,
105

Довольно старый пост, но я потратил на это час или два, поэтому я хотел поделиться своим выводом, тем более что некоторые из других перечисленных комментариев не совсем правильные.

TL; DR

Дайте дочерней таблице чужую или измените существующую, добавив ondelete='CASCADE':

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))

И одно из следующих отношений:

а) Это в родительской таблице:

children = db.relationship('Child', backref='parent', passive_deletes=True)

б) Или это в дочерней таблице:

parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

подробности

Во-первых, несмотря на то, что говорится в принятом ответе, отношения родитель / потомок не устанавливаются с помощью relationship, они устанавливаются с помощью ForeignKey. Вы можете поместить его relationshipв родительскую или дочернюю таблицы, и он будет работать нормально. Хотя, очевидно, в дочерних таблицах вы должны использовать backrefфункцию в дополнение к аргументу ключевого слова.

Вариант 1 (желательно)

Во-вторых, SqlAlchemy поддерживает два разных типа каскадирования. Первый и тот, который я рекомендую, встроен в вашу базу данных и обычно принимает форму ограничения на объявление внешнего ключа. В PostgreSQL это выглядит так:

CONSTRAINT child_parent_id_fkey FOREIGN KEY (parent_id)
REFERENCES parent_table(id) MATCH SIMPLE
ON DELETE CASCADE

Это означает, что при удалении записи из базы данных parent_tableвсе соответствующие строки child_tableбудут удалены за вас. Это быстро и надежно и, вероятно, ваш лучший выбор. Вы устанавливаете это в SqlAlchemy ForeignKeyследующим образом (часть определения дочерней таблицы):

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

Это ondelete='CASCADE'та часть, которая создает ON DELETE CASCADEна столе.

Попался!

Здесь есть важная оговорка. Обратите внимание, как я relationshipуказал passive_deletes=True? Если у вас этого нет, все не будет работать. Это связано с тем, что по умолчанию при удалении родительской записи SqlAlchemy делает что-то действительно странное. Он устанавливает для внешних ключей всех дочерних строк значение NULL. Итак, если вы удалите строку из parent_tablewhere id= 5, она в основном выполнит

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Я понятия не имею, зачем тебе это нужно. Я был бы удивлен, если бы многие движки баз данных даже позволили вам установить действительный внешний ключ NULL, создавая сироту. Вроде плохая идея, но, возможно, есть вариант использования. В любом случае, если вы позволите SqlAlchemy сделать это, вы не позволите базе данных очищать дочерние элементы, используя ON DELETE CASCADEнастроенный вами. Это потому, что он полагается на эти внешние ключи, чтобы знать, какие дочерние строки следует удалить. Как только SqlAlchemy установит их все NULL, база данных не сможет их удалить. Установка passive_deletes=Trueпредотвращает выдачу NULLвнешних ключей SqlAlchemy .

Вы можете узнать больше о пассивных удалениях в документации SqlAlchemy .

Вариант 2

Другой способ сделать это - позволить SqlAlchemy сделать это за вас. Это настраивается с использованием cascadeаргумента relationship. Если у вас есть связь, определенная в родительской таблице, она выглядит так:

children = relationship('Child', cascade='all,delete', backref='parent')

Если отношения на ребенке, вы делаете это так:

parent = relationship('Parent', backref=backref('children', cascade='all,delete'))

Опять же, это дочерний элемент, поэтому вам нужно вызвать вызываемый метод backrefи поместить туда каскадные данные.

При этом, когда вы удаляете родительскую строку, SqlAlchemy фактически запускает операторы удаления, чтобы вы могли очистить дочерние строки. Скорее всего, это будет не так эффективно, как позволить этой базе данных обрабатывать, если для вас, поэтому я не рекомендую это делать.

Вот документы SqlAlchemy о поддерживаемых каскадных функциях.

D512
источник
Спасибо за объяснение. Теперь это имеет смысл.
Odin
1
Почему объявление a Columnв дочерней таблице как ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')не работает? Я ожидал, что потомки будут удалены, когда их строка родительской таблицы также будет удалена. Вместо этого SQLA либо устанавливает дочерние элементы в a, parent.id=NULLлибо оставляет их «как есть», но не удаляет. Это после первоначального определения relationshipв родительском элементе как children = relationship('Parent', backref='parent')or relationship('Parent', backref=backref('parent', passive_deletes=True)); DB показывает cascadeправила в DDL (доказательство концепции на основе SQLite3). Мысли?
code_dredd
1
Кроме того, я должен отметить, что когда я использую, backref=backref('parent', passive_deletes=True)я получаю следующее предупреждение: SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfпредполагаю, что passive_deletes=Trueпо какой-то причине ему не нравится использование в этих (очевидных) отношениях родитель-потомок один-ко-многим.
code_dredd
Отличное объяснение. Один вопрос - есть ли deleteлишнее cascade='all,delete'?
Загги,
1
@zaggi является deleteизбыточным cascade='all,delete', поскольку, согласно документации SQLAlchemy , allявляется синонимом для:save-update, merge, refresh-expire, expunge, delete
pmsoltani
7

Стивен прав в том, что вам нужно явно создать обратную ссылку, это приводит к тому, что каскад применяется к родительскому элементу (в отличие от его применения к дочернему, как в тестовом сценарии).

Однако определение отношения для Child НЕ заставляет sqlalchemy рассматривать Child как родителя. Не имеет значения, где определена связь (дочерняя или родительская), это внешний ключ, связывающий две таблицы, который определяет, какая из них является родительской, а какая дочерней.

Однако имеет смысл придерживаться одного соглашения, и на основе ответа Стивена я определяю все свои дочерние отношения с родителем.

Ларри Вейя
источник
6

Я тоже боролся с документацией, но обнаружил, что сами строки документации, как правило, проще, чем руководство. Например, если вы импортируете отношение из sqlalchemy.orm и выполняете help (Relationship), он предоставит вам все параметры, которые вы можете указать для каскада. Пуля для delete-orphanговорит:

если обнаружен элемент дочернего типа без родителя, пометьте его для удаления.
Обратите внимание, что этот параметр предотвращает сохранение ожидающего элемента дочернего класса без присутствия родителя.

Я понимаю, что ваша проблема была больше в том, как документация по определению родительско-дочерних отношений. Но похоже, что у вас также может быть проблема с параметрами каскада, потому что "all"include "delete". "delete-orphan"- единственный вариант, который не включен в "all".

непосвященный
источник
Использование help(..)на sqlalchemyобъектах очень помогает! Спасибо :-))) ! PyCharm ничего не показывает в контекстных доках и просто забыл проверить файл help. Большое тебе спасибо!
dmitry_romanov
5

Ответ Стивена твердый. Я хотел бы указать на дополнительный смысл.

Используя relationship, вы делаете уровень приложения (Flask) ответственным за ссылочную целостность. Это означает, что другие процессы, которые обращаются к базе данных не через Flask, такие как утилита базы данных или человек, напрямую подключающийся к базе данных, не будут испытывать этих ограничений и могут изменить ваши данные таким образом, чтобы нарушить логическую модель данных, над разработкой которой вы так усердно работали. .

По возможности используйте ForeignKeyподход, описанный d512 и Alex. Механизм БД очень хорош в том, чтобы действительно обеспечивать соблюдение ограничений (неизбежным образом), так что это, безусловно, лучшая стратегия для поддержания целостности данных. Единственный раз, когда вам нужно полагаться на приложение для обработки целостности данных, - это когда база данных не может их обработать, например, версии SQLite, которые не поддерживают внешние ключи.

Если вам нужно создать дополнительную связь между сущностями, чтобы включить поведение приложения, такое как навигация по отношениям родительско-дочерних объектов, используйте backrefвместе с ForeignKey.

Крис Джонсон
источник
2

Ответ Стевана идеален. Но если вы все еще получаете ошибку. Другая возможная попытка на вершине этого была бы -

http://vincentaudebert.github.io/python/sql/2015/10/09/cascade-delete-sqlalchemy/

Скопировано по ссылке -

Подсказка, если у вас возникнут проблемы с зависимостью внешнего ключа, даже если вы указали каскадное удаление в своих моделях.

Используя SQLAlchemy, чтобы указать каскадное удаление, которое вы должны иметь cascade='all, delete'в родительской таблице. Хорошо, но тогда, когда вы выполните что-то вроде:

session.query(models.yourmodule.YourParentTable).filter(conditions).delete()

Фактически это вызывает ошибку о внешнем ключе, используемом в ваших дочерних таблицах.

Решение, которое я использовал для запроса объекта, а затем его удаления:

session = models.DBSession()
your_db_object = session.query(models.yourmodule.YourParentTable).filter(conditions).first()
if your_db_object is not None:
    session.delete(your_db_object)

Это должно удалить вашу родительскую запись И все связанные с ней дочерние элементы.

Прашант Момале
источник
1
.first()Требуется звонок ? Какие условия фильтрации возвращают список объектов, и все нужно удалить? Разве вызов не .first()получает только первый объект? @Prashant
Кавин Раджу С
2

Ответ Алекса Окрушко почти сработал для меня. Используется ondelete = 'CASCADE' и passive_deletes = True вместе. Но мне пришлось сделать что-то еще, чтобы он работал для sqlite.

Base = declarative_base()
ROOM_TABLE = "roomdata"
FURNITURE_TABLE = "furnituredata"

class DBFurniture(Base):
    __tablename__ = FURNITURE_TABLE
    id = Column(Integer, primary_key=True)
    room_id = Column(Integer, ForeignKey('roomdata.id', ondelete='CASCADE'))


class DBRoom(Base):
    __tablename__ = ROOM_TABLE
    id = Column(Integer, primary_key=True)
    furniture = relationship("DBFurniture", backref="room", passive_deletes=True)

Не забудьте добавить этот код, чтобы убедиться, что он работает для sqlite.

from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlite3 import Connection as SQLite3Connection

@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
    if isinstance(dbapi_connection, SQLite3Connection):
        cursor = dbapi_connection.cursor()
        cursor.execute("PRAGMA foreign_keys=ON;")
        cursor.close()

Украдено отсюда: язык выражений SQLAlchemy и SQLite при каскаде удаления

stupidstudent
источник
0

TL; DR: если вышеуказанные решения не работают, попробуйте добавить в столбец nullable = False.

Я хотел бы добавить здесь небольшой момент для некоторых людей, у которых может не получиться заставить каскадную функцию работать с существующими решениями (которые великолепны). Основное различие между моей работой и примером в том, что я использовал автокарту. Я не знаю точно, как это может помешать настройке каскадов, но хочу отметить, что использовал его. Я также работаю с базой данных SQLite.

Я пробовал каждое решение, описанное здесь, но для строк в моей дочерней таблице по-прежнему был установлен нулевой внешний ключ, когда родительская строка была удалена. Я пробовал все решения здесь безрезультатно. Однако каскад сработал, как только я установил для дочернего столбца с внешним ключом значение nullable = False.

В дочернюю таблицу я добавил:

Column('parent_id', Integer(), ForeignKey('parent.id', ondelete="CASCADE"), nullable=False)
Child.parent = relationship("parent", backref=backref("children", passive_deletes=True)

При такой настройке каскад работал так, как ожидалось.

Спенсер Уэстон
источник