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

Відповіді:


184

Проблема полягає в тому, що sqlalchemy вважає 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)


6
Ага, це все. Я б хотів, щоб документація про це була більш чітка!
Карл

15
Ага. Дуже корисний. У мене завжди були проблеми з документацією SQLAlchemy.
аяз

1
Це добре пояснено в поточному doc docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc,

1
@Lyman Zerga: у прикладі ОП: якщо ви видаляєте Childоб'єкт parent.children, чи слід видалити цей об’єкт із бази даних, або слід видалити лише його посилання на батьківський (тобто встановити parentidстовпець на нуль, замість видалення рядка)
Стівен

1
Зачекайте, relationshipце не диктує налаштування батьків-дитини. Використання ForeignKeyна столі - це те, що встановлює його як дитина. Не має значення, чи relationshipє у батька чи дитини.
d512

110

@ Asvenwer Steven хороший, коли ви видаляєте, через 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 років, але читання документа виглядає як pasive_delete = Правда, все-таки правильна річ.
Олексій Окрушко

2
Я можу підтвердити, що passive_deletes=Trueв цьому сценарії працює правильно.
d512

У мене виникли проблеми з алембічними редакторами автоматичного генерування, які включали каскад при видаленні - така відповідь була.
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_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 про каскадні функції, які він підтримує.


Дякую за пояснення. Зараз це має сенс.
Одін

1
Чому також оголошення Columnв дочірній таблиці ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')не працює? Я очікував, що дітей буде видалено, коли також буде видалено рядок батьківської таблиці. Натомість SQLA або встановлює дітей на a, parent.id=NULLабо залишає їх "як є", але не видаляє. Це після того, як спочатку було визначено relationshipв батьківській мові як children = relationship('Parent', backref='parent')або relationship('Parent', backref=backref('parent', passive_deletes=True)); БД показує 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'?
zaggi

1
@zaggi deleteIS зайвий cascade='all,delete', оскільки згідно з документами SQLAlchemy , allє синонімом до:save-update, merge, refresh-expire, expunge, delete
pmsoltani

7

Стівен вірно в тому, що вам потрібно чітко створити backref, це призводить до того, що каскад буде застосований до батьків (на відміну від того, щоб він застосовувався до дитини, як у тестовому сценарії).

Однак визначення взаємовідносин з дитиною НЕ змушує sqlalchemy вважати дитину батьком. Не має значення, де визначаються відносини (дитина або батько), його зовнішній ключ, який пов'язує дві таблиці, визначає, хто є батьком, а хто - дитина.

Хоча має сенс дотримуватися однієї конвенції, і, виходячи з відповіді Стівена, я визначаю всі стосунки дитини з батьком.


6

Я також боровся з документацією, але виявив, що самі документи є легшими, ніж посібник. Наприклад, якщо ви імпортуєте взаємозв'язок з sqlalchemy.orm і зробите довідку (співвідношення), він дасть вам усі варіанти, які ви можете вказати для каскаду. Куля delete-orphanговорить:

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

Я усвідомлюю, що ваше питання було пов'язане з тим, як було оформлено документацію для визначення стосунків батько-дитина. Але здавалося, що у вас також можуть виникнути проблеми з каскадними параметрами, оскільки "all"включає "delete". "delete-orphan"це єдиний варіант, який не включений у "all".


Використання help(..)на sqlalchemyоб'єктах допомагає багато! Дякую :-))) ! PyCharm нічого не показує в контекстних доках, і просто забув перевірити help. Дуже дякую!
dmitry_romanov

5

Відповідь Стівена тверда. Я хотів би зазначити додаткове значення.

За допомогою relationship , ви перетворюєте на додаток шар (колбу), відповідальний за цілісність референції. Це означає, що інші процеси, які отримують доступ до бази даних не через колбу, як утиліта бази даних або людина, яка безпосередньо підключається до бази даних, не зазнають цих обмежень і можуть змінити ваші дані таким чином, що порушує логічну модель даних, яку ви так важко працювали, щоб розробити .

По можливості використовуйте ForeignKeyпідхід, описаний d512 та Alex. Двигун DB дуже добре справляється з обмеженнями (неминуче), тому це, безумовно, найкраща стратегія збереження цілісності даних. Єдиний раз, коли вам потрібно покластися на додаток для обробки цілісності даних, це коли база даних не може обробити їх, наприклад версії 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
Kavin Raju S

2

Відповідь Олексія Окрушка майже найкраще працювала для мене. Використовується ondelete = 'CASCADE' та pasive_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 на каскаді видалення


0

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

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

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

На дитячому столі я додав:

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

За допомогою цього налаштування каскад функціонував, як очікувалося.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.