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되지 않은 경우에도 상위 항목에서 "제거 된"하위 항목도 삭제됨).

편집 : 그냥 발견 다음과 같은 경우 정말 상의 관계를 정의 할 Child클래스를, 당신이 그렇게 할 수 있습니다,하지만 당신은 캐스케이드 정의해야합니다 역 참조에를 (명시 적으로 역 참조를 작성하여), 다음과 같이 :

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

(함축 from sqlalchemy.orm import backref)


6
아하, 이거 다. 문서가 이것에 대해 더 명시 적이기를 바랍니다!
carl

15
찬성. 매우 유용합니다. 저는 항상 SQLAlchemy의 문서에 문제가있었습니다.
ayaz

1
이것은 물론 현재 문서에 설명되어 docs.sqlalchemy.org/en/rel_0_9/orm/cascades.html
에폭

1
@Lyman Zerga : OP의 예 :에서 Child개체 를 제거하는 경우 parent.children해당 개체를 데이터베이스에서 삭제해야하거나 부모에 대한 참조 만 제거해야합니다 (예 : parentid행을 삭제하는 대신 열을 null로 설정 )
스티븐

1
잠깐, relationship부모-자식 설정을 지시하지 않습니다. ForeignKey테이블에서 사용 하는 것이 아이로 설정하는 것입니다. 그것이 relationship부모인지 자식 인지는 중요하지 않습니다 .
d512

110

@Steven의 asnwer는 session.delete()제 경우에는 결코 발생하지 않는 삭제를 할 때 좋습니다 . 나는 대부분의 시간을 통해 삭제한다는 것을 알았습니다 session.query().filter().delete()(메모리에 요소를 넣지 않고 db에서 직접 삭제합니다). 이 방법을 사용하면 sqlalchemy cascade='all, delete'가 작동하지 않습니다. 하지만 해결책이 있습니다 : ON DELETE CASCADEdb를 통해 (참고 : 모든 데이터베이스가 지원하는 것은 아닙니다).

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로 설정 됨)되었으므로 데이터베이스 캐스케이드가 아무 작업도 수행하지 않았습니다.
Milorad Pop-Tosic 2015

@ MiloradPop-Tosic SQLAlchemy를 3 년 넘게 사용하지 않았지만 문서를 읽는 것은 passive_deletes = True처럼 보입니다. 여전히 옳은 일입니다.
Alex Okrushko 2015

2
passive_deletes=True이 시나리오에서 제대로 작동하는지 확인할 수 있습니다.
d512

삭제시 캐스케이드를 포함하는 alembic 자동 생성 개정판에 문제가있었습니다. 이것이 답이었습니다.
JNW

105

꽤 오래된 게시물이지만 한두 시간을 보냈으므로 특히 나열된 다른 댓글 중 일부가 옳지 않기 때문에 내 결과를 공유하고 싶었습니다.

TL; DR

자식 테이블에 외부 테이블을 지정하거나 기존 테이블을 수정하여 ondelete='CASCADE'다음을 추가합니다 .

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

그리고 다음 관계 중 하나 :

a) 상위 테이블에 다음이 있습니다.

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

b) 또는 자식 테이블에서 다음을 수행하십시오.

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이 데이터베이스에 의해 삭제됩니다. 빠르고 신뢰할 수 있으며 아마도 최선의 방법입니다. 다음 ForeignKey과 같이 SqlAlchemy에서 설정합니다 (하위 테이블 정의의 일부).

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는 실제로 자식 행을 정리하기 위해 delete 문을 실행합니다. 이 데이터베이스를 처리하는 것만 큼 효율적이지 않을 수 있으므로 권장하지 않습니다.

다음은 지원하는 계단식 기능에 대한 SqlAlchemy 문서 입니다.


설명 해주셔서 감사합니다. 이제 말이됩니다.
Odin

1
Column자식 테이블에서 a 를 선언하는 것도 작동하지 않는 이유는 무엇 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)). DB는 cascadeDDL (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 deleteSQLAlchemy의 문서에 따르면 다음 과 같은 동의어 cascade='all,delete'이므로 에서 중복됩니다 .allsave-update, merge, refresh-expire, expunge, delete
pmsoltani

7

스티븐은 명시 적으로 역 참조를 생성해야한다는 점에서 정확합니다. 이로 인해 계단식이 부모에 적용됩니다 (테스트 시나리오에서와 같이 자식에 적용되는 것과 반대).

그러나 Child에 대한 관계를 정의한다고해서 sqlalchemy가 Child를 부모로 간주하지는 않습니다. 관계가 정의 된 위치 (하위 또는 상위), 상위 및 하위를 결정하는 두 테이블을 연결하는 외래 키는 중요하지 않습니다.

그러나 하나의 관습을 고수하는 것이 합리적이며 Steven의 응답에 따라 모든 자녀 관계를 부모에 대해 정의하고 있습니다.


6

문서화도 힘들었지 만 독 스트링 자체가 매뉴얼보다 쉬운 경향이 있다는 것을 알았습니다. 예를 들어, sqlalchemy.orm에서 관계를 가져오고 help (relationship)을 수행하면 cascade에 대해 지정할 수있는 모든 옵션이 제공됩니다. 글 머리 기호 delete-orphan:

부모가없는 자식 유형의 항목이 감지되면 삭제 표시를합니다.
이 옵션은 부모가없는 상태에서 자식 클래스의 보류중인 항목이 유지되는 것을 방지합니다.

나는 당신의 문제가 부모-자녀 관계를 정의하는 문서 방식에 더 많은 것을 알고 있습니다. 그러나 "all"포함 하기 때문에 캐스케이드 옵션에 문제가있는 것 같습니다 "delete". "delete-orphan"에 포함되지 않은 유일한 옵션입니다 "all".


개체 help(..)에 사용 sqlalchemy하면 많은 도움이됩니다! 감사 :-))) ! PyCharm은 컨텍스트 도크에 아무것도 표시하지 않으며 help. 정말 고마워!
dmitry_romanov

5

스티븐의 대답은 확실합니다. 추가적인 의미를 지적하고 싶습니다.

를 사용 relationship하면 참조 무결성을 담당하는 앱 레이어 (Flask)를 만들 수 있습니다. 즉, 데이터베이스 유틸리티 또는 데이터베이스에 직접 연결하는 사람과 같이 Flask를 통하지 않고 데이터베이스에 액세스하는 다른 프로세스는 이러한 제약을 경험하지 않으며 설계하기 위해 열심히 작업 한 논리적 데이터 모델을 깨는 방식으로 데이터를 변경할 수 있습니다. .

가능하면 ForeignKeyd512와 Alex가 설명 하는 접근 방식을 사용하십시오 . DB 엔진은 (피할 수없는 방식으로) 제약을 진정으로 적용하는 데 매우 뛰어나므로 이것이 데이터 무결성을 유지하기위한 최고의 전략입니다. 데이터 무결성을 처리하기 위해 앱에 의존해야하는 유일한 경우는 데이터베이스가이를 처리 할 수없는 경우입니다 (예 : 외래 키를 지원하지 않는 SQLite 버전).

당신은 부모 - 자식 객체 관계, 사용, 탐색 등의 행동 응용 프로그램 수 있도록 개체 간의 추가 연결을 작성해야하는 경우 backref와 관련을 ForeignKey.


2

Stevan의 답변은 완벽합니다. 그러나 여전히 오류가 발생하는 경우. 그 위에 다른 가능한 시도는-

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

Alex Okrushko 대답은 거의 나에게 가장 잘 맞았습니다. 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의 삭제 캐스케이드


0

TLDR : 위의 솔루션이 작동하지 않으면 nullable = False를 열에 추가해보십시오.

캐스케이드 기능을 사용하여 기존 솔루션을 사용할 수없는 일부 사람들을 위해 여기에 작은 요점을 추가하고 싶습니다. 내 작업과 예제의 주요 차이점은 오토 맵을 사용했다는 것입니다. 이것이 캐스케이드 설정에 어떻게 방해가 될 수 있는지 정확히 알지 못하지만, 사용했음을 주목하고 싶습니다. 나는 또한 SQLite 데이터베이스로 작업하고 있습니다.

여기에 설명 된 모든 솔루션을 시도했지만 부모 행이 삭제되었을 때 자식 테이블의 행은 계속해서 외래 키를 null로 설정했습니다. 나는 여기에서 모든 해결책을 시도했지만 아무 소용이 없었습니다. 그러나 외래 키가있는 자식 열을 nullable = False로 설정하면 캐스케이드가 작동했습니다.

자식 테이블에 다음을 추가했습니다.

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

이 설정으로 캐스케이드가 예상대로 작동했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.