Relationship 뒤채우기
Relationship 뒤채우기 (Relationship back_populates)
이제 관계 속성을 사용해서 데이터베이스에서 서로 연결된 데이터를 다루는 방법을 알게 됐어요! 🎉 이번 장에서는 잠깐 한 걸음 물러나서, 우리가 그 Relationship() 속성들을 어떻게 정의했는지 다시 점검하면서 그 back_populates 인자가 뭘 하는 건지 명확하게 다뤄볼게요. 🤓
출처: 공식문서
back_populates가 있는 관계
각 Relationship() 안의 그 back_populates 인자는 대체 뭘까요?
그 값은 다른 모델 클래스에 있는 속성의 이름을 담은 문자열이에요.
이 값을 주면 SQLModel은 "이 모델에서 뭔가가 바뀌면, 다른 모델의 그 속성도 함께 바꿔야 한다"고 알게 되고, 심지어 세션으로 커밋하기 전에도 그렇게 동작해요(커밋은 데이터의 새로고침을 강제하거든요).
예시를 통해 더 잘 이해해 볼게요.
불완전한 관계
먼저 back_populates 없이 불완전한 버전을 작성해서 어떻게 동작하는지 봅시다:
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
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()
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)
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship()
# Code below omitted 👇
👀 전체 파일 미리보기 (Full file preview)
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
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()
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)
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship()
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", team=team_z_force
)
hero_rusty_man = Hero(
name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers
)
hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
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("Created hero:", hero_deadpond)
print("Created hero:", hero_rusty_man)
print("Created hero:", hero_spider_boy)
hero_spider_boy.team = team_preventers
session.add(hero_spider_boy)
session.commit()
session.refresh(hero_spider_boy)
print("Updated hero:", hero_spider_boy)
hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35)
hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E")
team_wakaland = Team(
name="Wakaland",
headquarters="Wakaland Capital City",
heroes=[hero_black_lion, hero_sure_e],
)
session.add(team_wakaland)
session.commit()
session.refresh(team_wakaland)
print("Team Wakaland:", team_wakaland)
hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32)
hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36)
hero_cap = Hero(
name="Captain North America", secret_name="Esteban Rogelios", age=93
)
team_preventers.heroes.append(hero_tarantula)
team_preventers.heroes.append(hero_dr_weird)
team_preventers.heroes.append(hero_cap)
session.add(team_preventers)
session.commit()
session.refresh(hero_tarantula)
session.refresh(hero_dr_weird)
session.refresh(hero_cap)
print("Preventers new hero:", hero_tarantula)
print("Preventers new hero:", hero_dr_weird)
print("Preventers new hero:", hero_cap)
def select_heroes():
with Session(engine) as session:
statement = select(Team).where(Team.name == "Preventers")
result = session.exec(statement)
team_preventers = result.one()
print("Preventers heroes:", team_preventers.heroes)
def update_heroes():
with Session(engine) as session:
hero_spider_boy = session.exec(
select(Hero).where(Hero.name == "Spider-Boy")
).one()
preventers_team = session.exec(
select(Team).where(Team.name == "Preventers")
).one()
print("Hero Spider-Boy:", hero_spider_boy)
print("Preventers Team:", preventers_team)
print("Preventers Team Heroes:", preventers_team.heroes)
hero_spider_boy.team = None
print("Spider-Boy without team:", hero_spider_boy)
print("Preventers Team Heroes again:", preventers_team.heroes)
session.add(hero_spider_boy)
session.commit()
print("After committing")
session.refresh(hero_spider_boy)
print("Spider-Boy after commit:", hero_spider_boy)
print("Preventers Team Heroes after commit:", preventers_team.heroes)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
update_heroes()
if __name__ == "__main__":
main()
데이터 객체 읽기
이제 Spider-Boy 히어로와, 별도로 Preventers 팀을 두 개의 select로 각각 가져와 볼게요.
이 부분이 어떻게 동작하는지는 이미 알기 때문에, select 문 · result 등으로 쪼개지 않고 한 번의 호출로 간결하게 쓸게요:
# Code above omitted 👆
def update_heroes():
with Session(engine) as session:
hero_spider_boy = session.exec(
select(Hero).where(Hero.name == "Spider-Boy")
).one()
preventers_team = session.exec(
select(Team).where(Team.name == "Preventers")
).one()
# Code below omitted 👇
💡 팁
직접 코드를 작성할 때는 아마 이 스타일을 가장 자주 쓰게 될 거예요. 더 짧고 편리하면서도 자동 완성과 인라인 오류 검사는 그대로 누릴 수 있으니까요.
데이터 출력하기
이제 현재의 Spider-Boy, 현재의 Preventers 팀, 특히 현재 Preventers 팀의 hero 목록을 출력해 볼게요:
# Code above omitted 👆
def update_heroes():
with Session(engine) as session:
hero_spider_boy = session.exec(
select(Hero).where(Hero.name == "Spider-Boy")
).one()
preventers_team = session.exec(
select(Team).where(Team.name == "Preventers")
).one()
print("Hero Spider-Boy:", hero_spider_boy)
print("Preventers Team:", preventers_team)
print("Preventers Team Heroes:", preventers_team.heroes)
# Code below omitted 👇
여기까지는 전부 좋아요. 😊 특히 preventers_team.heroes를 출력한 결과는 이랬어요:
Preventers Team Heroes: [
Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2),
Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2),
Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2),
Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2),
Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]
목록에 Spider-Boy가 포함되어 있는 걸 볼 수 있어요.
커밋 전에 객체 업데이트하기
이제 Spider-Boy를 업데이트해서, hero_spider_boy.team = None으로 설정해 팀에서 제외한 다음 이 객체를 다시 출력해 볼게요:
# Code above omitted 👆
def update_heroes():
with Session(engine) as session:
# Code here omitted 👈
hero_spider_boy.team = None
print("Spider-Boy without team:", hero_spider_boy)
print("Preventers Team Heroes again:", preventers_team.heroes)
# Code below omitted 👇
여기서 중요한 첫 번째 사실은, 아직 히어로를 커밋하지 않았기 때문에 hero 목록에 접근해도 자동 새로고침이 일어나지 않는다는 점이에요.
하지만 우리 코드에서는, 이 시점에 이미 "Spider-Boy는 더 이상 Preventers 소속이 아니다"라고 선언했죠. 🔥
💡 팁
나중에 세션을 커밋하지 않음으로써 되돌릴 수도 있지만, 지금 우리가 관심 있는 건 그게 아니에요.
코드의 이 지점에서, 메모리 안에서는 Preventers에 Spider-Boy가 포함되지 않을 것을 기대하고 있는 거예요.
hero_spider_boy를 팀 없이 출력한 결과는 이랬어요:
Spider-Boy without team: name='Spider-Boy' age=None id=3 secret_name='Pedro Parqueador' team_id=2 team=None
좋아요, team은 None으로 설정됐고, team_id 속성은 저장하기 전까지 팀 ID를 여전히 갖고 있어요. 하지만 지금 우리는 주로 관계 속성과 객체를 다루고 있으니 괜찮아요. ✅
그럼 이제 preventers_team.heroes를 출력하면 어떻게 될까요?
Preventers Team Heroes again: [
Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2),
Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2, team=None),
Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2),
Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2),
Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]
어머나! 😱 Spider-Boy가 아직 목록에 남아 있네요!
커밋하고 출력하기
이제 커밋하고 다시 출력해 보면:
# Code above omitted 👆
def update_heroes():
with Session(engine) as session:
# Code here omitted 👈
session.add(hero_spider_boy)
session.commit()
print("After committing")
session.refresh(hero_spider_boy)
print("Spider-Boy after commit:", hero_spider_boy)
print("Preventers Team Heroes after commit:", preventers_team.heroes)
# Code below omitted 👇
커밋 후에 preventers_team.heroes에 접근하면 새로고침이 일어나서, Spider-Boy가 빠진 최신 목록을 얻을 수 있어요. 괜찮아졌죠:
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, hero.team_id AS hero_team_id
FROM hero
WHERE ? = hero.team_id
2021-08-13 11:15:24,658 INFO sqlalchemy.engine.Engine [cached since 0.1924s ago] (2,)
Preventers Team Heroes after commit: [
Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2),
Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2),
Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2),
Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]
커밋 후에는 Spider-Boy가 없으니 좋아요. 😊
하지만 아까 그 시점에는 여전히 불일치가 있었죠.
커밋하기 전에 객체를 사용한다면 오류가 생길 수 있어요. 😔
그걸 고쳐 볼게요. 🤓
back_populates로 고치기
그것이 바로 back_populates가 존재하는 이유예요. ✨
다시 추가해 볼게요:
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
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="team")
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)
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
# Code below omitted 👇
그리고 나머지 코드는 그대로 두면 돼요:
# Code above omitted 👆
def update_heroes():
with Session(engine) as session:
# Code here omitted 👈
hero_spider_boy.team = None
print("Spider-Boy without team:", hero_spider_boy)
print("Preventers Team Heroes again:", preventers_team.heroes)
# Code below omitted 👇
💡 팁
이건 아까
hero_spider_boy.team을None으로 업데이트했지만 아직 커밋하지 않은 바로 그 부분이에요.아까 문제가 생겼던 바로 그 부분이죠.
결과 확인하기
이번에는 SQLModel(정확히는 SQLAlchemy)이 그 변화를 알아채고, 커밋하기 전에도 팀의 hero 목록을 자동으로 업데이트해 줄 거예요.
두 번째 출력은 이렇게 나올 거예요:
Preventers Team Heroes again: [
Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2),
Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2),
Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2),
Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]
이제 Spider-Boy가 목록에 없는 걸 볼 수 있어요. back_populates 덕분에 고쳤어요! 🎉
back_populates의 값
이제 왜 back_populates가 필요한지 알았으니, 그 정확한 값을 다시 살펴볼게요.
아주 단순한 코드, 그냥 문자열 하나지만, 정확히 어떤 문자열을 넣어야 할지 생각해 보면 헷갈릴 수 있어요:
from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select
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="team")
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)
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
# Code below omitted 👇
back_populates의 문자열은 현재 모델을 가리키는, 다른 모델의 속성 이름이에요.
그러니까 Team 클래스에는 heroes 속성이 있고, 그걸 Relationship(back_populates="team")으로 선언해요.
# Code above omitted 👆
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="team")
# Code below omitted 👇
여기서 back_populates="team"의 문자열 "team"은 다른 클래스(여기선 Hero)의 team 속성을 가리켜요.
그리고 Hero 클래스에는 team 속성을 Relationship(back_populates="heroes")으로 선언해요.
그러니 문자열 "heroes"는 Team 클래스의 heroes 속성을 가리키는 거예요.
# Code above omitted 👆
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)
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
# Code below omitted 👇
💡 팁
각 관계 속성은
back_populates를 통해 다른 모델의 상대 속성을 가리켜요.단순한 코드지만 생각해 보면 헷갈릴 수 있어요 😵, 왜냐하면 같은 줄에 두 모델 모두와 관련된 개념이 여러 곳에 섞여 있기 때문이에요:
- 현재 모델에 있다는 것만으로 이미 현재 모델과 관련이 있어요.
- 속성의 이름은 다른 모델에 대한 거예요.
- 타입 어노테이션도 다른 모델에 대한 거예요.
- 그리고
back_populates는 다른 모델의 속성 중, 현재 모델을 가리키는 속성을 가리켜요.
back_populates를 기억하는 머릿속 요령
기억할 수 있는 머릿속 요령 하나를 소개할게요. back_populates의 문자열은 항상 지금 편집 중인 현재 모델 클래스에 관한 것이에요. 🤓
그러니까 Hero 클래스 안에 있다면, 다른 어떤 테이블(다른 어떤 모델 — Team이 될 수도, Weapon, Powers 등이 될 수도 있어요)과 연결하는 어떤 관계 속성의 back_populates 값이든, 그 값은 여전히 항상 이 현재 클래스(Hero) 를 가리켜요.
그래서 back_populates는 아마 거의 "hero"나 "heroes" 같은 값일 거예요.
# Code above omitted 👆
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)
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
weapon_id: int | None = Field(default=None, foreign_key="weapon.id")
weapon: Weapon | None = Relationship(back_populates="hero")
powers: list[Power] = Relationship(back_populates="hero")
# Code below omitted 👇