본문 바로가기

Tech/SQLAlchemy

[SQLAlchemy] Relationship - CASCADE

반응형

SQLAlchemy 사용하면 여러 테이블간의 relationship 생성하여 운영할 있다. 테이블들의 relationship 다루어 운영을 하다보면 하나의 테이블에 대한 동작이 다른 테이블에 영향을 주는 경우가 발생한다. 이러한 경우 DB 다루기 위해 복잡하고 반복적인 작업이 필요할 있다. SQLAlchemy 에서는 관계가 맺어진 객체들간의 작업을 간편하고 자동화하기 위한 "relationship cascade" 라는 기능을 제공한다. 기능을 사용하면 relationship 가진 테이블간에 어떻게 동작할 것인지, 어떻게 영향을 끼칠 것인지를 정의할 있다.

1. Cascade 란?

Cascade 는 SQLAlchemy ORM 기능으로 객체들간의 관계를 정의할 때 사용할 수 있는 설정이다. cascade 설정을 통해서 관계가 정의된 객체들이 있을 때 한쪽 객체의 변경이나 삭제가 발생한 경우에 다른 쪽의 객체들의 테이블과 관련된 database 동작을 설정할 수 있다.

 

게시판의 사용자와 사용자가 작성한 글을 의미하는 User 와 Post 라는 테이블들을 예시로 생각해보자. 이들을 객체로 구현하면 1대N 관계를 가지게 된다. 한명의 user 는 여러개의 post 를 작성할 수 있다.  이러한 상황에서 user 가 삭제되는 경우 user 가 작성한 post 는 어떻게 되야할까? delete 쿼리를 통해서 해당 user 가 작성한 post 도 함께 삭제되도록 하거나, 작성한 user 없이도 post 를 유지하도록 할 수 있다. 이러한 동작을 설정하는 기능이 "relationshipt cascade" 이다.

2. Cascade 종류

SQLAlchemy 에서 relationship cascade 로 설정할 수 있는 옵션들은 다음과 같다.

- all

all 옵션은 아래에서 설명될 옵션들중 delete-orphan 을 제외한 모든 옵션들 (save-update, merge, refresh-expire, expunge, delete) 을 모두 합친 옵션이다. 보통 cascade 설정으로 all 또는 all, delete-orphan 으로 많이 사용한다.

- save-update

save-update 옵션은 객체가 session.add() 기능을 통해 session 에 반영될때 해당 객체와 관련된 객체들도 같은 Session 에 반영되도록 하는 옵션이다.

 

User 와 Post 객체의 예시에서 User 객체의 posts 에 Post 객체를 추가한 후에 User 를 session 에 반영하는 경우 User 객체뿐만 아니라 Post 객체들도 암시적으로 해당 session 에 반영된다.

 

save-update 는 이미 session 에 포함되어 있는 객체들에도 영향을 끼친다. 만약 이미 session 에 반영된 User 객체에 새로운 Post 객체를 생성하여 추가하면 따로 session.add() 를 호출하여 session 에 반영하지 않아도 새로운 Post 객체가 session 에 포함되게 된다.

 

save-update 옵션을 양방향 관계에서 사용하는 경우 양쪽 객체가 session 에 반영이 되어있는지 여부에 따라서 동작 결과가 달라질 수 있다. 예를들어 session 에 추가된 객체 o1 과 추가되지 않는 객체 o2 가 있다. 이때 o1 의 items 컬렉션에 o2 를 추가하는 경우에는 o2 가 session 에 자동으로 반영되지만, 반대로 o2 에 o1 을 추가하는 경우에는 o2 가 session 에 추가되지 않는다. 이때는 명시적으로 o2 를 session 에 반영해주어야 한다.

- delete

delete 옵션은 부모 객체가 삭제되었을때, 해당 부모의 자식 객체들도 삭제되도록 하는 옵션이다.

 

만약 User Post delete 옵션으로 relationship cascade 가 지정되어 있다면, 특정 User 가 삭제될때, User 가 작성한 Post 들도 모두 삭제된다. 만약 cascade delete 를 지정하지 않았다면 Post 에서 User 를 보고있는 Foreign key NULL 로 변경되고 Post 객체가 삭제되지는 않는다.

 

1N 관계에서는 보통 delete-orphan 옵션과 함께 많이 사용한다. delete-ophan delete 와 달리 부모와 자식간의 관계가 끊어졌을때도 자식 객체를 삭제한다. 그렇기 때문에 delete delete-orphan 은 같이 사용하면 부모 객체가 삭제되었을때, 또는 부모 객체로부터 자식 객체가 분리되었을때 모두 자식 객체가 삭제되도록 할 수 있다.

 

- delete-orphan

 

delete-orphan 옵션은 delete 옵션에 다른 동작을 추가한 옵션이다. delete 의 동작에 더해서, 만약 자식 객체가 부모 객체로부터 분리되는 경우에도 자식 객체가 삭제되도록 한다. 이 옵션은 부모가 자식을 소유하는 형태의 관계이어서 자식 객체의 FK 가 NULL 이 되는 것을 허용하지 않는 상황에서 많이 사용한다.

 

이러한 상황은 보통 자식이 부모를 하나만 가질 수 밖에 없는 1대N 관계에서 많이 발생하며 다른 관계 형식에서는 많이 나타나지 않는다.

- merge

merge 옵션은 session.merge() 동작시에 parent 에서 참조하는 객체로 session.merge() 동작이 영향을 끼친다.

- refresh-expire

refresh-expire 옵션은 흔하지 않는 옵션이다. 이 옵션은 부모 객체가 session.expire() 동작시에 자식 객체에게 영향을 준다. 만약 session.refresh() 사용사에는 자식 객체가 만료되기만 하고 refresh 는 안됨

- expunge

expunge 옵션은 부모 객체가 session.expunge() 기능을 통해서 session 에서 삭제될때 자식 객체들에게도 동일한 동작이 수행된다.

3. CASCADE 예제

CASCADE 의 종류는 위 항목에서 정리한 것처럼 많지만, 보통 all, delete-orphan 두가지를 조합하여 사용하는 경우가 많은 것 같다. 이 둘을 함께 사용하게 되면 부모 객체에서 발생하는 변경이 모두 자식에게 적용되며, 부모 객체에서 발생하는 변경 삭제뿐만 아니라 delete-orphan 옵션으로 인해 부모와 자식이 관계가 분리되는 경우에도 자식 객체가 삭제되도록 한다.

 

예제에서는 1:N 관계의 두 객체 Parent 와 Child 를 두고 비교하려한다. 두 객체의 엔티티는 다음과 같다.

 

class Parent(Base):
    __tablename__ = "parent"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)
    children = relationship(
        "Child",
        back_populates="parent",
        cascade="all, delete-orphan",
    )

class Child(Base)
    __tablename__ = "child"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)
    parent_id = mapped_column(Integer, ForeignKey("parent.id"))
    parent = relationship(
        "Parent",
        back_populates="children",
    )

 

두 객체는 sqlalchemy 의 명시적 매핑으로 구현했으며, child 테이블이 parent_id 를 FK 로 참조하고 있다. relationship cascade 는 "all, delete-orphan" 으로 설정하였다.

 

# parent 객체와 children 객체 생성
parent: Parent = Parent(name="parent")
children: list[Child] = [
  Child(name="child1", parent_id=parent.id),
  Child(name="child2", parent_id=parent.id),
  Child(name="child3", parent_id=parent.id),
]

# parent 에 children 추가
parent.children = children

# parent 와 children 모두 아직 session 에 추가되지 않음
assert not parent in session
for child in children
  assert not child in session

# parent 객체를 session 에 추가
session.add(parent)
session.commit()

# parent 와 children 모두가 session 에 추가됨을 확인
assert parent in session
for child in children:
  assert child in session

 

첫번째 에제는 parent 에 child 객체들을 추가한 다음 session.add() 를 호출하여 parent 객체를 session 에 추가했을때의 상황이다.

 

먼저 parent 와 children 을 생성한 후에 parent 에 children 을 추가하였다. 이때까지는 parent 와 children 모두 session 에 추가되지 않았다. 이후 parent 를 session 에 추가하고 commit 한 후에는 parent 와 child 모두가 session 에 추가된 것을 확인할 수 있다.

 

이렇게 동작하는 이유는 all 옵션에 save-update 옵션이 포함되어 있기 때문이다. 만약 save-update 또는 all 옵션이 설정되어있지 않는 경우에는 parent 는 session 에 추가되지만 children 에 있는 child 객체들은 추가되지 않는다.

 

# parent 객체에서 child 객체를 제거 (detach)
parent.children.remove(children[0])
session.add(parent)
session.commit()

# children[0] 이 session 과 DB 에서 제거됨을 확인
# session 에서 제거됨
assert children[0] not in session

# parent 에서 제거됨
assert len(parent.children) == 2
assert children[0] not in parent.children

# DB 에서 조회되지 않음
assert session.query(Child).filter_by(parent_id=parent.id).count() == 2
assert session.query(Child).filter_by(id=children[0].id).count() == 0

 

다음은 parent 의 children 에서 child 객체를 삭제했을때의 상황이다. 이전 예제에서 parent.children 에 children 의 객체들을 추가하였다. 이 예제에서는 그 중 0번재 인덱스에 있는 객체를 parent 에서 삭제한다.  이후 session.add() 를 통해 parent 의 변경사항을 session 에 적용한다. 그 다음 assert 문들을 통해서 children[0] 이 parent 에서 제거됐을뿐 아니라 session 에서도 삭제되었음을 확인할 수 있다.

 

parent 의 children 에서 child 객체가 삭제되어 session 에 적용이 되면 child 객체는 parent, parent_id 가 모두 None 이 된다. 이러한 상태를 orphan 이라고 한다. delete-orphan 옵션은 orphan 상태가 된 객체를 삭제한다. 그렇게 때문에 parent 객체와 분리된 children[0] 객체는 session 에서 삭제되게 된다.

[Reference]

- https://docs.sqlalchemy.org/en/20/orm/cascades.html

 

Cascades — SQLAlchemy 2.0 Documentation

Cascades Mappers support the concept of configurable cascade behavior on relationship() constructs. This refers to how operations performed on a “parent” object relative to a particular Session should be propagated to items referred to by that relation

docs.sqlalchemy.org

 

반응형

'Tech > SQLAlchemy' 카테고리의 다른 글

[SQLAlchemy] imperative mapping  (0) 2023.11.11
[SQLAlchemy] 연관관계 설정  (0) 2022.07.23
[SQLAlchemy] SQLAlchemy 기본 설명  (0) 2022.07.17