다대다 관계로 데이터 만들기

다대다 관계로 데이터 만들기 (Create Data with 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()

출처: 공식문서

히어로 만들기

전처럼 create_heroes() 함수를 만들고, 그 안에서 팀과 히어로를 만들게요:

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

이건 지금까지 해온 것과 아주 비슷해요.

팀 두 개를 만들고, 히어로 세 명을 만들어요.

유일하게 새로운 세부사항은 team 인자 대신 teams를 쓴다는 거예요. 그게 새 관계 속성의 이름이니까요. 그리고 더 중요하게, 팀 리스트를 넘겨요(단일 팀이라 하더라도).

Deadpond가 이제 두 팀에 속하는 게 보이죠?

커밋, 새로고침, 프린트

이제 전처럼 해 볼게요. 세션commit하고, 데이터를 refresh하고, 프린트해요:

        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)

main에 추가하기

전처럼, 이 프로그램을 커맨드 라인에서 실행할 때 호출되도록 create_heroes() 함수를 main() 함수에 추가해요:

def main():
    create_db_and_tables()
    create_heroes()

프로그램 실행하기

커맨드 라인에서 프로그램을 실행하면 이렇게 출력돼요:

$ uv run python app.py

// 이전 출력은 생략했어요 🙈

// 자동으로 새 트랜잭션 시작
INFO Engine BEGIN (implicit)
// 먼저 히어로 데이터를 삽입
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [generated in 0.00041s] ('Deadpond', 'Dive Wilson', None)
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [cached since 0.001942s ago] ('Rusty-Man', 'Tommy Sharp', 48)
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [cached since 0.002541s ago] ('Spider-Boy', 'Pedro Parqueador', None)
// 두 번째로 팀 데이터를 삽입
INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?)
INFO Engine [generated in 0.00037s] ('Z-Force', 'Sister Margaret's Bar')
INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?)
INFO Engine [cached since 0.001239s ago] ('Preventers', 'Sharp Tower')
// 생성된 ID를 재사용할 수 있도록 링크 데이터를 마지막에 삽입
INFO Engine INSERT INTO heroteamlink (team_id, hero_id) VALUES (?, ?)
INFO Engine [generated in 0.00026s] ((2, 3), (1, 1), (2, 1), (2, 2))
// 커밋하고 데이터를 데이터베이스에 저장
INFO Engine COMMIT

// 자동으로 새 트랜잭션 시작
INFO Engine BEGIN (implicit)
// 데이터 새로고침
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00019s] (1,)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.001959s ago] (2,)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.003215s ago] (3,)

// Deadpond 프린트
Deadpond: name='Deadpond' age=None id=1 secret_name='Dive Wilson'

// .team 속성에 접근하면 새로고침이 일어남
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 [generated in 0.00025s] (1,)

// Deadpond의 팀을 프린트, 팀 2개! 🎉
Deadpond teams: [Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar'), Team(id=2, name='Preventers', headquarters='Sharp Tower')]

// Rusty-Man 프린트
Rusty-Man: name='Rusty-Man' age=48 id=2 secret_name='Tommy Sharp'

// .team 속성에 접근하면 새로고침이 일어남
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.001716s ago] (2,)

// Rusty-Man 팀을 프린트, 하나지만 여전히 리스트
Rusty-Man Teams: [Team(id=2, name='Preventers', headquarters='Sharp Tower')]

// Spider-Boy 프린트
Spider-Boy: name='Spider-Boy' age=None id=3 secret_name='Pedro Parqueador'

// .team 속성에 접근하면 새로고침이 일어남
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.002739s ago] (3,)

// Spider-Boy의 팀을 프린트, 하나지만 여전히 리스트
Spider-Boy Teams: [Team(id=2, name='Preventers', headquarters='Sharp Tower')]

// with 블록이 끝날 때 이전 자동 트랜잭션을 자동으로 롤백
INFO Engine ROLLBACK

요약

모델 링크를 설정하고 나면, 관계 속성으로 사용하는 건 꽤 간단해요. 그냥 파이썬 객체일 뿐이니까요. ✨

더 알아보기 (Learn more)