SQLAlchemy: xóa theo tầng


116

Tôi phải thiếu một cái gì đó tầm thường với các tùy chọn xếp tầng của SQLAlchemy bởi vì tôi không thể có được một thao tác xóa tầng đơn giản để hoạt động chính xác - nếu một phần tử mẹ bị xóa, các phần tử con vẫn tồn tại, với nullcác khóa ngoại.

Tôi đã đặt một trường hợp thử nghiệm ngắn gọn ở đây:

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

Đầu ra:

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

Có một mối quan hệ đơn giản, một-nhiều giữa Cha mẹ và Con cái. Tập lệnh tạo cha mẹ, thêm 3 con, sau đó cam kết. Tiếp theo, nó xóa cha mẹ, nhưng con cái vẫn tồn tại. Tại sao? Làm cách nào để xóa các tầng con?


Phần này trong tài liệu (ít nhất là bây giờ, 3 năm sau bài đăng ban đầu) có vẻ khá hữu ích về điều này: docs.sqlalchemy.org/en/rel_0_9/orm/session.html#cascades
Soferio

Câu trả lời:


183

Vấn đề là sqlalchemy coi nó Childnhư là cha mẹ, bởi vì đó là nơi bạn xác định mối quan hệ của mình (tất nhiên nó không quan tâm đến việc bạn gọi nó là "Con").

Nếu bạn xác định mối quan hệ trên Parentlớp thay vào đó, nó sẽ hoạt động:

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

(lưu ý "Child"dưới dạng một chuỗi: điều này được cho phép khi sử dụng kiểu khai báo, để bạn có thể tham chiếu đến một lớp chưa được xác định)

Bạn cũng có thể muốn thêm delete-orphan( deletekhiến con bị xóa khi cha mẹ bị xóa, delete-orphancũng xóa mọi con đã bị "xóa" khỏi cha, ngay cả khi cha mẹ không bị xóa)

CHỈNH SỬA: vừa phát hiện ra: nếu bạn thực sự muốn xác định mối quan hệ trên Childlớp, bạn có thể làm như vậy, nhưng bạn sẽ phải xác định tầng trên backref (bằng cách tạo backref một cách rõ ràng), như thế này:

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

(ngụ ý from sqlalchemy.orm import backref)


6
Aha, đây là nó. Tôi ước tài liệu rõ ràng hơn về điều này!
carl

15
Phải. Rất hữu ích. Tôi luôn gặp vấn đề với tài liệu của SQLAlchemy.
ayaz

1
Điều này được giải thích rõ ràng trong tài liệu hiện tại docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
Epoc

1
@Lyman Zerga: trong ví dụ của OP: nếu bạn xóa một Childđối tượng khỏi parent.children, liệu đối tượng đó có bị xóa khỏi cơ sở dữ liệu hay chỉ nên xóa tham chiếu đến cấp độ gốc (tức là đặt parentidcột thành null, thay vì xóa hàng)
Steven

1
Chờ đã, relationshipkhông ra lệnh cho thiết lập cha-con. Sử dụng ForeignKeytrên bàn là những gì thiết lập nó như một đứa trẻ. Không quan trọng nếu relationshiplà phụ huynh hay con cái.
d512

110

@ Steven's asnwer rất tốt khi bạn đang xóa thông qua session.delete()đó không bao giờ xảy ra trong trường hợp của tôi. Tôi nhận thấy rằng hầu hết thời gian tôi xóa qua session.query().filter().delete()(không đưa các phần tử vào bộ nhớ và xóa trực tiếp khỏi db). Sử dụng phương pháp này sqlalchemy's cascade='all, delete'không hoạt động. Tuy nhiên, có một giải pháp: ON DELETE CASCADEthông qua db (lưu ý: không phải tất cả cơ sở dữ liệu đều hỗ trợ nó).

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
Cám ơn giải thích sự khác biệt này - tôi đã cố gắng để sử dụng session.query().filter().delete()và phải vật lộn để tìm vấn đề
nighthawk454

4
Tôi đã phải thiết lập passive_deletes='all'để làm cho con bị xóa bởi thác cơ sở dữ liệu khi cha mẹ bị xóa. Với passive_deletes=True, các đối tượng con đã bị tách rời (cha mẹ được đặt thành NULL) trước khi cha mẹ bị xóa, do đó, tầng cơ sở dữ liệu không làm được gì.
Milorad Pop-Tosic

@ MiloradPop-Tosic Tôi đã không sử dụng SQLAlchemy hơn 3 năm nhưng đọc tài liệu có vẻ như passive_deletes = True vẫn là điều đúng đắn.
Alex Okrushko

2
Tôi có thể xác nhận rằng passive_deletes=True nó hoạt động chính xác trong trường hợp này.
d512

Tôi đã gặp sự cố với các bản sửa đổi tự động tạo alembic bao gồm phân tầng khi xóa - đây là câu trả lời.
JNW

105

Bài đăng khá cũ, nhưng tôi chỉ dành một hoặc hai giờ cho việc này, vì vậy tôi muốn chia sẻ phát hiện của mình, đặc biệt là vì một số nhận xét khác được liệt kê không hoàn toàn đúng.

TL; DR

Đặt cho bảng con một bảng ngoại lai hoặc sửa đổi bảng hiện có, thêm ondelete='CASCADE':

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

một trong những mối quan hệ sau:

a) Cái này trên bảng mẹ:

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

b) Hoặc cái này trên bảng con:

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

Chi tiết

Trước hết, mặc dù câu trả lời được chấp nhận nói gì, mối quan hệ cha / con không được thiết lập bằng cách sử dụng relationship, nó được thiết lập bằng cách sử dụng ForeignKey. Bạn có thể đặt relationshipbảng cha hoặc bảng con và nó sẽ hoạt động tốt. Mặc dù, rõ ràng trên các bảng con, bạn phải sử dụngbackref hàm ngoài đối số từ khóa.

Tùy chọn 1 (ưu tiên)

Thứ hai, SqlAlchemy hỗ trợ hai kiểu xếp tầng khác nhau. Cái đầu tiên, và cái tôi khuyên dùng, được xây dựng trong cơ sở dữ liệu của bạn và thường có dạng một ràng buộc đối với khai báo khóa ngoại. Trong PostgreSQL, nó trông như thế này:

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

Điều này có nghĩa là khi bạn xóa một bản ghi parent_table, thì tất cả các hàng tương ứng trong đó child_tablesẽ bị cơ sở dữ liệu xóa cho bạn. Nó nhanh chóng và đáng tin cậy và có lẽ là đặt cược tốt nhất của bạn. Bạn đã thiết lập điều này trong SqlAlchemy thông quaForeignKey như thế này (một phần của định nghĩa bảng con):

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

Các ondelete='CASCADE' là phần tạo ra ON DELETE CASCADEtrên bàn.

Gotcha!

Có một cảnh báo quan trọng ở đây. Chú ý làm thế nào tôi có một relationshipđược chỉ định với passive_deletes=True? Nếu bạn không có điều đó, toàn bộ mọi thứ sẽ không hoạt động. Điều này là do theo mặc định khi bạn xóa một bản ghi mẹ, SqlAlchemy làm một điều gì đó thực sự kỳ lạ. Nó đặt các khóa ngoại của tất cả các hàng con thành NULL. Vì vậy, nếu bạn xóa một hàng khỏi parent_tablevị trí id= 5, thì về cơ bản nó sẽ thực thi

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Tại sao bạn muốn điều này, tôi không biết. Tôi sẽ rất ngạc nhiên nếu nhiều công cụ cơ sở dữ liệu thậm chí cho phép bạn đặt một khóa ngoại hợp lệ NULL, tạo ra một đứa trẻ mồ côi. Có vẻ như một ý tưởng tồi, nhưng có thể có một trường hợp sử dụng. Dù sao, nếu bạn để SqlAlchemy làm điều này, bạn sẽ ngăn cơ sở dữ liệu không thể dọn dẹp các phần tử con bằng cách ON DELETE CASCADEbạn thiết lập. Điều này là do nó dựa vào các khóa ngoại đó để biết hàng con nào cần xóa. Khi SqlAlchemy đã đặt tất cả chúng thành NULL, cơ sở dữ liệu không thể xóa chúng. Việc thiết lập passive_deletes=Truengăn chặn SqlAlchemy NULLnhập các khóa ngoại.

Bạn có thể đọc thêm về xóa thụ động trong tài liệu SqlAlchemy .

Lựa chọn 2

Một cách khác bạn có thể làm là để SqlAlchemy làm điều đó cho bạn. Điều này được thiết lập bằng cách sử dụng cascadeđối số củarelationship . Nếu bạn có mối quan hệ được xác định trên bảng mẹ, nó sẽ giống như sau:

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

Nếu mối quan hệ là trên trẻ em, bạn làm điều đó như sau:

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

Một lần nữa, đây là con nên bạn phải gọi một phương thức có tên backref và đưa dữ liệu tầng vào đó.

Với điều này, khi bạn xóa một hàng mẹ, SqlAlchemy sẽ thực sự chạy các câu lệnh xóa để bạn xóa các hàng con. Điều này có thể sẽ không hiệu quả bằng việc để cơ sở dữ liệu này xử lý nếu đối với bạn, vì vậy tôi không khuyên bạn nên dùng nó.

Đây là tài liệu SqlAlchemy về các tính năng xếp tầng mà nó hỗ trợ.


Cảm ơn bạn đã giải thích. Bây giờ nó có ý nghĩa.
Odin

1
Tại sao việc khai báo a Columntrong bảng con cũng ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')không hoạt động? Tôi mong đợi những đứa trẻ sẽ bị xóa khi hàng trong bảng cha của chúng cũng bị xóa. Thay vào đó, SQLA đặt các con thành a parent.id=NULLhoặc để chúng "nguyên trạng", nhưng không xóa. Đó là sau khi xác định ban đầu relationshiptrong cha là children = relationship('Parent', backref='parent')hoặc relationship('Parent', backref=backref('parent', passive_deletes=True)); DB hiển thị cascadecác quy tắc trong DDL (bằng chứng khái niệm dựa trên SQLite3). Suy nghĩ?
code_dredd

1
Ngoài ra, tôi cần lưu ý rằng khi tôi sử dụng, backref=backref('parent', passive_deletes=True)tôi nhận được cảnh báo sau:, SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfgợi ý rằng nó không thích việc sử dụng passive_deletes=Truetrong mối quan hệ cha-con (rõ ràng) này vì một số lý do.
code_dredd

Lời giải thích tuyệt vời. Một câu hỏi - là deletethừa trong cascade='all,delete'?
zaggi

1
@zaggi deleteLÀ thừa cascade='all,delete', vì theo tài liệu của SQLAlchemy , alllà từ đồng nghĩa với:save-update, merge, refresh-expire, expunge, delete
pmsoltani

7

Steven đúng ở chỗ bạn cần tạo backref một cách rõ ràng, điều này dẫn đến việc phân tầng được áp dụng trên cha mẹ (trái ngược với việc nó được áp dụng cho con như trong kịch bản thử nghiệm).

Tuy nhiên, việc xác định mối quan hệ trên Child KHÔNG làm cho sqlalchemy coi Child là cha mẹ. Không quan trọng mối quan hệ được xác định ở đâu (con hay cha), khóa ngoại của nó liên kết hai bảng xác định cái nào là cha và cái nào là con.

Tuy nhiên, thật hợp lý khi tuân theo một quy ước và dựa trên phản hồi của Steven, tôi đang xác định tất cả các mối quan hệ con cái của mình dựa trên cha mẹ.


6

Tôi cũng phải vật lộn với tài liệu hướng dẫn, nhưng nhận thấy rằng bản thân các chuỗi tài liệu có xu hướng dễ dàng hơn so với hướng dẫn. Ví dụ: nếu bạn nhập mối quan hệ từ sqlalchemy.orm và thực hiện trợ giúp (mối quan hệ), nó sẽ cung cấp cho bạn tất cả các tùy chọn mà bạn có thể chỉ định cho tầng. Dấu đầu dòng cho delete-orphannói:

nếu một mục thuộc loại của trẻ không có cha mẹ được phát hiện, hãy đánh dấu mục đó để xóa.
Lưu ý rằng tùy chọn này ngăn không cho một mục đang chờ xử lý của lớp trẻ được duy trì mà không có phụ huynh hiện diện.

Tôi nhận thấy vấn đề của bạn nhiều hơn với cách tài liệu xác định mối quan hệ cha mẹ-con cái. Nhưng có vẻ như bạn cũng có thể gặp vấn đề với các tùy chọn phân tầng, vì "all"bao gồm "delete". "delete-orphan"là tùy chọn duy nhất không được bao gồm trong "all".


Sử dụng help(..)trên các sqlalchemyđối tượng giúp ích rất nhiều! Cảm ơn :-))) ! PyCharm không hiển thị gì trong các dock ngữ cảnh và rõ ràng là đã quên kiểm tra help. Cảm ơn bạn rất nhiều!
dmitry_romanov

5

Câu trả lời của Steven là chắc chắn. Tôi muốn chỉ ra một hàm ý bổ sung.

Bằng cách sử dụng relationship , bạn đang làm cho lớp ứng dụng (Flask) chịu trách nhiệm về tính toàn vẹn của tham chiếu. Điều đó có nghĩa là các quy trình khác truy cập cơ sở dữ liệu không thông qua Flask, chẳng hạn như tiện ích cơ sở dữ liệu hoặc người kết nối trực tiếp với cơ sở dữ liệu, sẽ không gặp phải những ràng buộc đó và có thể thay đổi dữ liệu của bạn theo cách phá vỡ mô hình dữ liệu logic mà bạn đã dày công thiết kế .

Bất cứ khi nào có thể, hãy sử dụng ForeignKeyphương pháp được mô tả bởi d512 và Alex. Công cụ DB rất giỏi trong việc thực sự thực thi các ràng buộc (theo cách không thể tránh khỏi), vì vậy đây là chiến lược tốt nhất để duy trì tính toàn vẹn của dữ liệu. Lần duy nhất bạn cần dựa vào một ứng dụng để xử lý tính toàn vẹn của dữ liệu là khi cơ sở dữ liệu không thể xử lý chúng, ví dụ: các phiên bản SQLite không hỗ trợ khóa ngoại.

Nếu bạn cần tạo thêm liên kết giữa các thực thể để cho phép các hành vi ứng dụng như điều hướng mối quan hệ đối tượng cha-con, hãy sử dụng backrefkết hợp với ForeignKey.


2

Câu trả lời của Stevan là hoàn hảo. Nhưng nếu bạn vẫn nhận được lỗi. Cách khác có thể thử trên đó sẽ là -

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

Đã sao chép từ liên kết-

Mẹo nhanh nếu bạn gặp rắc rối với phụ thuộc khóa ngoại ngay cả khi bạn đã chỉ định xóa theo tầng trong các mô hình của mình.

Sử dụng SQLAlchemy, để chỉ định xóa theo tầng mà bạn nên có cascade='all, delete'trên bảng mẹ của mình. Ok nhưng sau đó khi bạn thực thi một cái gì đó như:

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

Nó thực sự gây ra lỗi về khóa ngoại được sử dụng trong bảng con của bạn.

Giải pháp tôi đã sử dụng nó để truy vấn đối tượng và sau đó xóa nó:

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)

Thao tác này sẽ xóa bản ghi mẹ của bạn VÀ tất cả các bản ghi con được liên kết với nó.


1
.first()cần gọi không? Điều kiện lọc nào trả về danh sách các đối tượng và mọi thứ phải bị xóa? Không phải gọi .first()là đối tượng đầu tiên? @Prashant
Kavin Raju S

2

Câu trả lời của Alex Okrushko gần như phù hợp nhất với tôi. Đã sử dụng ondelete = 'CASCADE' và passive_deletes = True được kết hợp. Nhưng tôi đã phải làm thêm điều gì đó để làm cho nó hoạt động cho 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)

Đảm bảo thêm mã này để đảm bảo nó hoạt động cho 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()

Bị đánh cắp từ đây: Ngôn ngữ biểu thức SQLAlchemy và SQLite trên dòng thác xóa


0

TLDR: Nếu các giải pháp trên không hoạt động, hãy thử thêm nullable = False vào cột của bạn.

Tôi muốn thêm một điểm nhỏ ở đây cho một số người có thể không nhận được chức năng thác nước để làm việc với các giải pháp hiện có (rất tuyệt). Sự khác biệt chính giữa công việc của tôi và ví dụ là tôi đã sử dụng automap. Tôi không biết chính xác điều đó có thể cản trở việc thiết lập các tầng như thế nào, nhưng tôi muốn lưu ý rằng tôi đã sử dụng nó. Tôi cũng đang làm việc với cơ sở dữ liệu SQLite.

Tôi đã thử mọi giải pháp được mô tả ở đây, nhưng các hàng trong bảng con của tôi tiếp tục có khóa ngoại của chúng được đặt thành rỗng khi hàng mẹ bị xóa. Tôi đã thử tất cả các giải pháp ở đây nhưng không có kết quả. Tuy nhiên, phân tầng hoạt động khi tôi đặt cột con có khóa ngoại thành nullable = False.

Trên bảng con, tôi đã thêm:

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

Với thiết lập này, tầng hoạt động như mong đợi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.