SQLAlchemy: penghapusan kaskade

116

Saya pasti melewatkan sesuatu yang sepele dengan opsi kaskade SQLAlchemy karena saya tidak bisa menghapus kaskade sederhana untuk beroperasi dengan benar - jika elemen induk dihapus, anak-anak tetap ada, dengan nullkunci asing.

Saya telah menempatkan kasus uji singkat di sini:

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()

Keluaran:

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

Ada hubungan sederhana satu-ke-banyak antara Induk dan Anak. Skrip membuat induk, menambahkan 3 anak, lalu melakukan. Selanjutnya, ini menghapus induknya, tetapi anaknya tetap ada. Mengapa? Bagaimana cara menghapus kaskade anak?

carl
sumber
Bagian dalam dokumen ini (setidaknya sekarang, 3 tahun kemudian setelah kiriman asli) tampaknya cukup membantu dalam hal ini: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Jawaban:

183

Masalahnya adalah sqlalchemy dianggap Childsebagai orang tua, karena di sanalah Anda mendefinisikan hubungan Anda (tidak peduli Anda menyebutnya "Anak" tentunya).

Jika Anda mendefinisikan hubungan di Parentkelas, ini akan berhasil:

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

(catatan "Child"sebagai string: ini diperbolehkan saat menggunakan gaya deklaratif, sehingga Anda bisa merujuk ke kelas yang belum ditentukan)

Anda mungkin ingin menambahkan delete-orphanjuga ( deletemenyebabkan anak dihapus saat induknya dihapus, delete-orphanjuga menghapus semua anak yang "dihapus" dari induknya, meskipun induk tidak dihapus)

EDIT: baru saja mengetahui: jika Anda benar - benar ingin menentukan hubungan di Childkelas, Anda dapat melakukannya, tetapi Anda harus menentukan kaskade di backref (dengan membuat backref secara eksplisit), seperti ini:

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

(menyiratkan from sqlalchemy.orm import backref)

Steven
sumber
6
Aha, ini dia. Saya berharap dokumentasinya lebih eksplisit tentang ini!
carl
15
Iya. Sangat membantu. Saya selalu memiliki masalah dengan dokumentasi SQLAlchemy.
ayaz
1
Ini dijelaskan dengan baik dalam dokumen docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc
1
@ Lyman Zerga: dalam contoh OP: jika Anda menghapus Childobjek dari parent.children, apakah objek itu harus dihapus dari database, atau hanya referensi ke induknya saja yang dihapus (misalnya, setel parentidkolom ke null, alih-alih menghapus baris)
Steven
1
Tunggu, relationshiptidak mendikte penyiapan orang tua-anak. Menggunakan di ForeignKeyatas meja adalah yang mengaturnya sebagai anak. Tidak masalah jika relationshipada pada orang tua atau anak.
d512
110

@ Jawaban Steven bagus saat Anda menghapus session.delete()yang tidak pernah terjadi dalam kasus saya. Saya perhatikan bahwa sebagian besar waktu saya menghapus session.query().filter().delete()(yang tidak memasukkan elemen ke dalam memori dan menghapus langsung dari db). Menggunakan metode ini sqlalchemy cascade='all, delete'tidak berfungsi. Namun ada solusinya: ON DELETE CASCADEmelalui db (catatan: tidak semua database mendukungnya).

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)
Alex Okrushko
sumber
3
Terima kasih telah menjelaskan perbedaan ini - Saya mencoba menggunakan session.query().filter().delete()dan berjuang untuk menemukan masalahnya
nighthawk454
4
Saya harus mengatur passive_deletes='all'untuk mendapatkan anak-anak yang akan dihapus oleh kaskade database ketika orang tua dihapus. Dengan passive_deletes=True, objek turunan dipisahkan (set induk ke NULL) sebelum induk dihapus, sehingga kaskade database tidak melakukan apa-apa.
Milorad Pop-Tosic
@ MiloradPop-Tosic Saya belum pernah menggunakan SQLAlchemy selama lebih dari 3 tahun tetapi membaca dokumen sepertinya passive_deletes = True masih merupakan hal yang benar.
Alex Okrushko
2
Saya dapat mengonfirmasi bahwa passive_deletes=Trueberfungsi dengan benar dalam skenario ini.
d512
Saya mengalami masalah dengan revisi pembuatan otomatis alembik yang mencakup kaskade saat menghapus - inilah jawabannya.
JNW
105

Pos yang cukup lama, tetapi saya hanya menghabiskan satu atau dua jam untuk ini, jadi saya ingin membagikan temuan saya, terutama karena beberapa komentar lain yang tercantum kurang tepat.

TL; DR

Berikan tabel anak asing atau modifikasi yang sudah ada, dengan menambahkan ondelete='CASCADE':

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

Dan salah satu dari hubungan berikut:

a) Ini di tabel induk:

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

b) Atau ini di meja anak:

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

Detail

Pertama, terlepas dari apa yang dikatakan jawaban yang diterima, hubungan orang tua / anak tidak dibuat dengan menggunakan relationship, itu dibuat dengan menggunakan ForeignKey. Anda dapat meletakkannya relationshipdi tabel induk atau anak dan itu akan bekerja dengan baik. Meskipun, tampaknya pada tabel anak, Anda harus menggunakan backreffungsi selain argumen kata kunci.

Opsi 1 (lebih disukai)

Kedua, SqlAlchemy mendukung dua jenis cascading. Yang pertama, dan yang saya rekomendasikan, dibangun ke dalam database Anda dan biasanya berbentuk batasan pada deklarasi kunci asing. Di PostgreSQL terlihat seperti ini:

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

Ini berarti bahwa ketika Anda menghapus record dari parent_table, maka semua baris terkait child_tableakan dihapus untuk Anda oleh database. Ini cepat dan andal dan mungkin taruhan terbaik Anda. Anda mengatur ini di SqlAlchemy melalui ForeignKeyseperti ini (bagian dari definisi tabel anak):

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

Ini ondelete='CASCADE'adalah bagian yang membuat di ON DELETE CASCADEatas meja.

Kena kau!

Ada peringatan penting di sini. Perhatikan bagaimana saya relationshipmenentukan dengan passive_deletes=True? Jika Anda tidak memilikinya, semuanya tidak akan berfungsi. Ini karena secara default ketika Anda menghapus catatan induk SqlAlchemy melakukan sesuatu yang sangat aneh. Ini menetapkan kunci asing dari semua baris anak ke NULL. Jadi jika Anda menghapus baris dari parent_tablewhere id= 5, maka pada dasarnya itu akan dijalankan

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Mengapa Anda menginginkan ini, saya tidak tahu. Saya akan terkejut jika banyak mesin database bahkan mengizinkan Anda untuk mengatur kunci asing yang valid NULL, membuat yatim piatu. Sepertinya ide yang buruk, tapi mungkin ada kasus penggunaan. Bagaimanapun, jika Anda membiarkan SqlAlchemy melakukan ini, Anda akan mencegah database untuk dapat membersihkan anak-anak menggunakan ON DELETE CASCADEyang Anda atur. Ini karena bergantung pada kunci asing tersebut untuk mengetahui baris anak mana yang akan dihapus. Setelah SqlAlchemy mengatur semuanya ke NULL, database tidak dapat menghapusnya. Mengatur passive_deletes=Truemencegah SqlAlchemy NULLkeluar dari kunci asing.

Anda dapat membaca lebih lanjut tentang penghapusan pasif di dokumen SqlAlchemy .

pilihan 2

Cara lain yang dapat Anda lakukan adalah membiarkan SqlAlchemy melakukannya untuk Anda. Ini diatur menggunakan cascadeargumen dari relationship. Jika Anda memiliki hubungan yang ditentukan pada tabel induk, terlihat seperti ini:

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

Jika hubungannya ada pada anak, lakukan seperti ini:

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

Sekali lagi, ini adalah anak sehingga Anda harus memanggil metode yang dipanggil backrefdan meletakkan data kaskade di sana.

Dengan ini, ketika Anda menghapus baris induk, SqlAlchemy akan benar-benar menjalankan pernyataan delete untuk Anda membersihkan baris anak. Ini kemungkinan tidak akan seefisien membiarkan database ini menangani jika untuk Anda jadi saya tidak merekomendasikannya.

Berikut adalah dokumen SqlAlchemy tentang fitur berjenjang yang didukungnya.

d512
sumber
Terima kasih atas penjelasannya. Masuk akal sekarang.
Odin
1
Mengapa mendeklarasikan a Columndi tabel anak sebagai ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')tidak berhasil? Saya berharap anak-anak akan dihapus ketika baris tabel induk mereka juga dihapus. Sebaliknya, SQLA menetapkan turunan ke a parent.id=NULLatau membiarkannya "sebagaimana adanya", tetapi tidak ada penghapusan. Itu setelah awalnya mendefinisikan relationshipdalam induk sebagai children = relationship('Parent', backref='parent')atau relationship('Parent', backref=backref('parent', passive_deletes=True)); DB menunjukkan cascadeaturan di DDL (bukti konsep berbasis SQLite3). Pikiran?
code_dredd
1
Juga, saya harus mencatat bahwa ketika saya menggunakan backref=backref('parent', passive_deletes=True)saya mendapatkan peringatan berikut:, SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfmenyarankan itu tidak suka penggunaan passive_deletes=Truedalam hubungan orang tua-anak satu-ke-banyak (jelas) ini untuk beberapa alasan.
code_dredd
Penjelasan yang bagus. Satu pertanyaan - apakah deletemubazir cascade='all,delete'?
zaggi
1
@zaggi deleteIS redundan cascade='all,delete', karena menurut dokumen SQLAlchemy , alladalah sinonim untuk:save-update, merge, refresh-expire, expunge, delete
pmsoltani
7

Steven benar karena Anda perlu membuat backref secara eksplisit, ini menghasilkan kaskade yang diterapkan pada induk (bukan diterapkan ke anak seperti dalam skenario pengujian).

Namun, mendefinisikan hubungan pada Child TIDAK membuat sqlalchemy menganggap Child sebagai orang tua. Tidak masalah di mana hubungan didefinisikan (anak atau induk), kunci asing yang menghubungkan dua tabel yang menentukan mana induk dan yang mana anak.

Masuk akal untuk tetap berpegang pada satu konvensi, dan berdasarkan tanggapan Steven, saya mendefinisikan semua hubungan anak saya dengan orang tua.

Larry Weya
sumber
6

Saya kesulitan dengan dokumentasinya juga, tetapi ternyata docstringnya sendiri cenderung lebih mudah daripada manual. Misalnya, jika Anda mengimpor hubungan dari sqlalchemy.orm dan melakukan bantuan (hubungan), ini akan memberi Anda semua opsi yang dapat Anda tentukan untuk kaskade. Peluru untuk delete-orphanmengatakan:

jika item berjenis anak tanpa orang tua terdeteksi, tandai untuk dihapus.
Perhatikan bahwa opsi ini mencegah item kelas anak yang tertunda bertahan tanpa kehadiran orang tua.

Saya menyadari masalah Anda lebih pada cara dokumentasi untuk mendefinisikan hubungan orang tua-anak. Tetapi tampaknya Anda mungkin juga mengalami masalah dengan opsi kaskade, karena "all"termasuk "delete". "delete-orphan"adalah satu-satunya opsi yang tidak termasuk dalam "all".

Duniawi
sumber
Menggunakan help(..)pada sqlalchemyobyek membantu banyak! Terima kasih :-)))! PyCharm tidak menampilkan apa pun di dok konteks, dan jelas lupa memeriksa file help. Terima kasih banyak!
dmitry_romanov
5

Jawaban Steven jelas. Saya ingin menunjukkan implikasi tambahan.

Dengan menggunakan relationship, Anda membuat lapisan aplikasi (Flask) bertanggung jawab atas integritas referensial. Itu berarti proses lain yang mengakses database tidak melalui Flask, seperti utilitas database atau orang yang terhubung ke database secara langsung, tidak akan mengalami kendala tersebut dan dapat mengubah data Anda dengan cara yang merusak model data logis yang telah Anda rancang dengan susah payah. .

Jika memungkinkan, gunakan ForeignKeypendekatan yang dijelaskan oleh d512 dan Alex. Mesin DB sangat pandai dalam benar-benar menegakkan batasan (dengan cara yang tidak dapat dihindari), jadi sejauh ini strategi terbaik untuk menjaga integritas data. Satu-satunya saat Anda perlu mengandalkan aplikasi untuk menangani integritas data adalah ketika database tidak dapat menanganinya, misalnya versi SQLite yang tidak mendukung kunci asing.

Jika Anda perlu membuat tautan lebih lanjut di antara entitas untuk mengaktifkan perilaku aplikasi seperti menavigasi hubungan objek induk-anak, gunakan backrefbersama dengan ForeignKey.

Chris Johnson
sumber
2

Jawaban dari Stevan sempurna. Tetapi jika Anda masih mendapatkan kesalahan. Percobaan lain yang mungkin di atas itu adalah -

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

Disalin dari tautan-

Tip cepat jika Anda mendapatkan masalah dengan ketergantungan kunci asing bahkan jika Anda telah menentukan penghapusan kaskade dalam model Anda.

Menggunakan SQLAlchemy, untuk menentukan penghapusan kaskade yang harus Anda miliki cascade='all, delete'di tabel induk Anda. Ok tapi kemudian ketika Anda menjalankan sesuatu seperti:

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

Ini sebenarnya memicu kesalahan tentang kunci asing yang digunakan di tabel anak Anda.

Solusi yang saya gunakan untuk menanyakan objek dan kemudian menghapusnya:

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)

Ini harus menghapus catatan induk Anda DAN semua anak yang terkait dengannya.

Momale Prashant
sumber
1
Apakah panggilan .first()diperlukan? Kondisi filter apa yang mengembalikan daftar objek, dan semuanya harus dihapus? Bukankah menelepon .first()hanya mendapat objek pertama? @Prashant
Kavin Raju S
2

Jawaban Alex Okrushko hampir bekerja paling baik untuk saya. Digunakan ondelete = 'CASCADE' dan passive_deletes = True digabungkan. Tapi saya harus melakukan sesuatu yang ekstra untuk membuatnya berfungsi untuk 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)

Pastikan untuk menambahkan kode ini untuk memastikannya berfungsi untuk 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()

Dicuri dari sini: bahasa ekspresi SQLAlchemy dan SQLite di delete cascade

siswa bodoh
sumber
0

TLDR: Jika solusi di atas tidak berfungsi, coba tambahkan nullable = False ke kolom Anda.

Saya ingin menambahkan poin kecil di sini untuk beberapa orang yang mungkin tidak mendapatkan fungsi kaskade untuk bekerja dengan solusi yang ada (yang hebat). Perbedaan utama antara pekerjaan saya dan contohnya adalah saya menggunakan automap. Saya tidak tahu persis bagaimana hal itu dapat mengganggu pengaturan kaskade, tetapi saya ingin mencatat bahwa saya menggunakannya. Saya juga bekerja dengan database SQLite.

Saya mencoba setiap solusi yang dijelaskan di sini, tetapi baris di tabel anak saya terus memiliki kunci asing yang disetel ke nol ketika baris induk dihapus. Saya telah mencoba semua solusi di sini tetapi tidak berhasil. Namun, kaskade berfungsi setelah saya mengatur kolom anak dengan kunci asing ke nullable = False.

Di meja anak, saya menambahkan:

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

Dengan pengaturan ini, kaskade berfungsi seperti yang diharapkan.

Spencer Weston
sumber