Подтвердить что ты не робот

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

Мне нужно пропустить что-то тривиальное с параметрами каскада 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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

@Steven asnwer хорош, когда вы удаляете 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

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

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_table, где id = 5, тогда она в основном будет выполняться

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

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

Подробнее о пассивных удалениях можно прочитать в документах 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 о каскадных функциях, которые он поддерживает.

Ответ 4

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

Скопировано из link-

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

Используя 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)

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

Ответ 8

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

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

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

На дочернем столе я добавил:

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

С этой настройкой каскад функционировал как ожидалось.