다대다 관계 갱신·제거
다대다 관계 갱신·제거 (Update and Remove Many-to-Many Relationships)
이제 이런 다대다 관계를 갱신하고 제거하는 법을 볼게요.
이전 코드에서 멈춘 지점부터 이어갈게요.
👀 전체 파일 미리 보기
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine
class HeroTeamLink(SQLModel, table=True):
team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True)
hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True)
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
headquarters: str
heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink)
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
engine = create_engine(sqlite_url, echo=True)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def create_heroes():
with Session(engine) as session:
team_preventers = Team(name="Preventers", headquarters="Sharp Tower")
team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")
hero_deadpond = Hero(
name="Deadpond",
secret_name="Dive Wilson",
teams=[team_z_force, team_preventers],
)
hero_rusty_man = Hero(
name="Rusty-Man",
secret_name="Tommy Sharp",
age=48,
teams=[team_preventers],
)
hero_spider_boy = Hero(
name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers]
)
session.add(hero_deadpond)
session.add(hero_rusty_man)
session.add(hero_spider_boy)
session.commit()
session.refresh(hero_deadpond)
session.refresh(hero_rusty_man)
session.refresh(hero_spider_boy)
print("Deadpond:", hero_deadpond)
print("Deadpond teams:", hero_deadpond.teams)
print("Rusty-Man:", hero_rusty_man)
print("Rusty-Man Teams:", hero_rusty_man.teams)
print("Spider-Boy:", hero_spider_boy)
print("Spider-Boy Teams:", hero_spider_boy.teams)
def main():
create_db_and_tables()
create_heroes()
if __name__ == "__main__":
main()
출처: 공식문서
갱신할 데이터 가져오기
update_heroes() 함수를 만들어 볼게요.
Spider-Boy와 Z-Force 팀을 가져올 거예요.
이런 과정은 이미 익숙하니 짧은 버전을 써서 데이터를 한 개의 파이썬 문장으로 가져올게요.
그리고 이제 select()를 쓰니 그것도 import 해야 해요.
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
def update_heroes():
with Session(engine) as session:
hero_spider_boy = session.exec(
select(Hero).where(Hero.name == "Spider-Boy")
).one()
team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one()
그리고 물론, update_heroes()를 우리의 main() 함수에 추가해야 해요:
def main():
create_db_and_tables()
create_heroes()
update_heroes()
다대다 관계 추가하기
이제 Spider-Boy가 Z-Force 팀이 정말 멋지다고 생각해서, 그곳에 가서 합류하기로 했다고 해 봅시다.
같은 관계 속성을 써서 hero_spider_boy를 team_z_force.heroes에 넣을 수 있어요:
def update_heroes():
with Session(engine) as session:
hero_spider_boy = session.exec(
select(Hero).where(Hero.name == "Spider-Boy")
).one()
team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one()
team_z_force.heroes.append(hero_spider_boy)
session.add(team_z_force)
session.commit()
print("Updated Spider-Boy's Teams:", hero_spider_boy.teams)
print("Z-Force heroes:", team_z_force.heroes)
!!! tip "팁"
커밋 직후 모델의 속성에 `hero_spider_boy.teams`와 `team_z_force.heroes`로 접근하고 있으니, 데이터가 자동으로 새로고침돼요.
그래서 `session.refresh()`를 호출할 필요가 없어요.
그런 다음 변경을 커밋하고, 갱신된 Spider-Boy의 팀을 프린트해서 확인해요.
세션에는 Z-Force만 add하고 커밋한다는 점을 눈여겨보세요.
Spider-Boy는 세션에 추가하지도 않고, 심지어 새로고침도 하지 않아요. 그래도 그의 팀을 프린트하죠.
이게 여전히 올바르게 동작하는 이유는 모델의 Relationship()에서 back_populates를 쓰고 있기 때문이에요. 덕분에 SQLModel(실제로는 SQLAlchemy)이 변경 사항과 갱신을 추적해서, 다른 관련 모델의 관계에도 그 변경이 일어나도록 보장할 수 있어요. 🎉
프로그램 실행하기
커맨드 라인에서 프로그램을 실행하면 전부 잘 동작하는지 확인할 수 있어요:
$ uv run python app.py
// 이전 출력은 생략했어요 🙈
// 새 다대다 관계 만들기
INFO Engine INSERT INTO heroteamlink (team_id, hero_id) VALUES (?, ?)
INFO Engine [generated in 0.00020s] (1, 3)
INFO Engine COMMIT
// 새 자동 트랜잭션 시작
INFO Engine BEGIN (implicit)
// .teams 속성에 접근하는 동안 데이터 자동 새로고침
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00044s] (3,)
INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters
FROM team, heroteamlink
WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id
INFO Engine [cached since 0.1648s ago] (3,)
// Z-Force를 포함한 Spider-Boy 팀 프린트 🎉
Updated Spider-Boy's Teams: [
Team(id=2, name='Preventers', headquarters='Sharp Tower'),
Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar')
]
// .heroes 속성에 접근하는 동안 데이터 자동 새로고침
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero, heroteamlink
WHERE ? = heroteamlink.team_id AND hero.id = heroteamlink.hero_id
INFO Engine [cached since 0.1499s ago] (1,)
// Spider-Boy를 포함한 Z-Force 히어로 프린트 🎉
Z-Force heroes: [
Hero(name='Deadpond', age=None, id=1, secret_name='Dive Wilson'),
Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', teams=[
Team(id=2, name='Preventers', headquarters='Sharp Tower'),
Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar', heroes=[...])
])
]
다대다 관계 제거하기
이제 Spider-Boy가 팀에 들어간 직후, 그들의 "생명 보존 정책"이 자기한테 익숙한 것보다 훨씬 느슨하다는 걸 깨달았다고 해 봅시다. 💀
게다가 산업 안전·보건도 그다지 좋지 않아요... 💥
그래서 Spider-Boy는 Z-Force를 떠나기로 결심해요.
관계를 갱신해서 hero_spider_boy.teams에서 team_z_force를 제거해 볼게요.
hero_spider_boy.teams는 그냥 리스트(SQLAlchemy가 관리하는 특별한 리스트지만 어쨌든 리스트)니까, 표준 리스트 메서드를 쓸 수 있어요.
이 경우 항목을 받아 리스트에서 제거하는 .remove() 메서드를 써요:
def update_heroes():
with Session(engine) as session:
hero_spider_boy = session.exec(
select(Hero).where(Hero.name == "Spider-Boy")
).one()
team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one()
team_z_force.heroes.append(hero_spider_boy)
session.add(team_z_force)
session.commit()
print("Updated Spider-Boy's Teams:", hero_spider_boy.teams)
print("Z-Force heroes:", team_z_force.heroes)
hero_spider_boy.teams.remove(team_z_force)
session.add(team_z_force)
session.commit()
print("Reverted Z-Force's heroes:", team_z_force.heroes)
print("Reverted Spider-Boy's teams:", hero_spider_boy.teams)
그리고 이번에는, 다시 한번 보여주기 위해, back_populates 덕분에 SQLModel(실제로는 SQLAlchemy)이 관계로 모델들을 연결해 준다는 점을 이용해요. 우리는 hero_spider_boy 객체에서 작업을 수행했는데도(hero_spider_boy.teams를 수정), 세션에는 team_z_force를 추가해요. 그리고 hero_spider_boy는 아예 추가하지도 않고 커밋해요.
이게 여전히 동작하는 이유는 hero_spider_boy의 팀을 갱신하면 back_populates로 동기화되어 team_z_force에도 그 변경이 반영되기 때문이에요. 그래서 그것도 DB에 저장할 변경 사항(Spider-Boy가 제거됐다는)을 갖게 되는 거죠.
그리고 팀을 추가하고 변경을 커밋하면 team_z_force 객체가 갱신되고, hero_spider_boy와 연결돼 있던 테이블도 바뀌었으니 그것도 내부적으로 갱신됨으로 표시돼서 전부 잘 동작해요.
그런 다음 전부 제대로 됐는지 확인하려고 다시 프린트해요.
프로그램 다시 실행하기
마지막 부분이 잘 됐는지 확인하려면 프로그램을 다시 실행하면 돼요. 이런 출력이 나올 거예요:
$ uv run python app.py
// 이전 출력은 생략했어요 🙈
// 링크 테이블의 행 삭제
INFO Engine DELETE FROM heroteamlink WHERE heroteamlink.team_id = ? AND heroteamlink.hero_id = ?
INFO Engine [generated in 0.00043s] (1, 3)
// 변경 사항 저장
INFO Engine COMMIT
// 자동으로 새 트랜잭션 시작
INFO Engine BEGIN (implicit)
// .heroes 속성에 접근하는 동안 데이터 자동 새로고침
INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters
FROM team
WHERE team.id = ?
INFO Engine [generated in 0.00029s] (1,)
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero, heroteamlink
WHERE ? = heroteamlink.team_id AND hero.id = heroteamlink.hero_id
INFO Engine [cached since 0.5625s ago] (1,)
// 변경을 되돌린 후의 Z-Force 히어로 프린트
Reverted Z-Force's heroes: [
Hero(name='Deadpond', age=None, id=1, secret_name='Dive Wilson')
]
// .teams 속성에 접근하는 동안 데이터 자동 새로고침
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.4209s ago] (3,)
INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters
FROM team, heroteamlink
WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id
INFO Engine [cached since 0.5842s ago] (3,)
// 변경을 되돌린 후의 Spider-Boy 팀 프린트
Reverted Spider-Boy's teams: [
Team(id=2, name='Preventers', headquarters='Sharp Tower')
]
// 저장되지 않았을 수 있는 이전 트랜잭션을 자동으로 롤백
INFO Engine ROLLBACK
요약
링크 모델과 관계 속성을 설정한 뒤에는 다대다 관계를 갱신하고 제거하는 게 꽤 간단해요.
흔한 리스트 연산을 쓰면 돼요. 🚀