관계 생성과 업데이트

관계 생성과 업데이트 (Create and Update Relationships)

이제 이 새로운 관계 속성들을 사용해서 데이터를 생성하는 방법을 살펴볼게요. ✨

출처: 공식문서

필드로 인스턴스 생성하기

먼저 예전에 우리가 히어로와 팀을 만들 때 썼던 코드를 확인해 볼게요:

# Code above omitted 👆

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")
        session.add(team_preventers)
        session.add(team_z_force)
        session.commit()

        hero_deadpond = Hero(
            name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id
        )
        hero_rusty_man = Hero(
            name="Rusty-Man",
            secret_name="Tommy Sharp",
            age=48,
            team_id=team_preventers.id,
        )
        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)

# Code below omitted 👇

👀 전체 파일 미리보기 (Full file preview)

from sqlmodel import Field, Session, SQLModel, create_engine


class Team(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    headquarters: str


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


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")
        session.add(team_preventers)
        session.add(team_z_force)
        session.commit()

        hero_deadpond = Hero(
            name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id
        )
        hero_rusty_man = Hero(
            name="Rusty-Man",
            secret_name="Tommy Sharp",
            age=48,
            team_id=team_preventers.id,
        )
        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)


def main():
    create_db_and_tables()
    create_heroes()


if __name__ == "__main__":
    main()

여기서 눈여겨볼 것들이 몇 가지 있어요.

첫째, 우리는 Team 인스턴스 객체들을 몇 개 만드는데, Hero 인스턴스들을 만들 때 team_id 필드에 이 팀들의 ID를 사용하고 싶어요.

하지만 모델 인스턴스는 세션에 추가하고 커밋하기 전까지는 데이터베이스가 생성한 ID를 갖고 있지 않아요. 그 전에는 그냥 None이죠. 그리고 우리는 실제 ID를 사용하고 싶어요.

그래서 Hero 인스턴스들을 만들기 시작하기 전에 먼저 팀들을 추가하고 세션을 커밋해야, 그들의 ID를 사용할 수 있어요.

그런 다음 그 ID들을 Hero 인스턴스를 만들 때 사용해요. 새 히어로들을 세션에 추가하고 나서 커밋하죠.

그래서 결국 두 번 커밋하게 되는 거예요. 그리고 어떤 것들을 먼저 추가한 뒤 커밋하고, 그 모든 걸 올바른 순서로 해야 해요. 그렇지 않으면 아직 저장되지 않아서 현재 Noneteam.id를 사용하게 될 수도 있거든요.

이 관계 속성들이 처음으로 도움이 되는 부분이 바로 여기예요. 🤓

관계 속성으로 인스턴스 생성하기

이제 그 모든 걸 하지만, 이번에는 새롭고 반짝이는 관계(Relationship) 속성을 사용해서 해볼게요.

# Code above omitted 👆

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)

# Code below omitted 👇

👀 전체 파일 미리보기 (Full file preview)

from sqlmodel import Field, Relationship, Session, SQLModel, create_engine


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


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


def main():
    create_db_and_tables()
    create_heroes()


if __name__ == "__main__":
    main()

이제 우리는 Team 인스턴스들을 만들고, Hero 인스턴스를 만들 때 그 새 team 인자에 직접 전달할 수 있어요. team_id=team_preventers.id 대신 team=team_preventers처럼요.

그리고 내부에서 SQLAlchemy가 어떻게 동작하는지 덕분에, 이 팀들은 아직 ID를 갖고 있지 않아도 돼요. 우리가 전체 객체를 각 히어로에 할당하니까, 그 팀들은 데이터베이스에 자동으로 생성되고, 자동 ID가 만들어져서 각각의 대응하는 히어로 행의 team_id 컬럼에 설정될 거예요.

사실 이제는 session.add(team)으로 팀들을 명시적으로 세션에 넣을 필요조차 없어요. 왜냐하면 이 Team 인스턴스들은 이미 우리가 세션에 추가하는 히어로들과 연결되어 있기 때문이죠.

SQLAlchemy는 히어로들을 올바르게 저장하려면 그 팀들도 다음 커밋에 포함해야 한다는 걸 알고 있어요.

그리고 보시다시피 이제는 commit() 하나만 하면 돼요.

관계 할당하기

우리가 hero.team_idteam.id라는 정수를 할당할 수 있었던 것과 같은 방식으로, hero.team에도 Team 인스턴스를 할당할 수 있어요:

# Code above omitted 👆

def create_heroes():
    with Session(engine) as session:

# Code here omitted 👈

        hero_spider_boy.team = team_preventers
        session.add(hero_spider_boy)
        session.commit()
        session.refresh(hero_spider_boy)
        print("Updated hero:", hero_spider_boy)

# Code below omitted 👇

히어로가 있는 팀 생성하기

앞에서는 Team 인스턴스들을 먼저 만든 뒤 Hero 인스턴스를 만들 때 team= 인자에 전달했어요.

반대로 Hero 인스턴스들을 먼저 만든 뒤, Team 인스턴스를 만들 때 리스트를 받는 heroes= 인자에 그들을 전달할 수도 있어요:

# Code above omitted 👆

def create_heroes():
    with Session(engine) as session:

# Code here omitted 👈

        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)

# Code below omitted 👇

여기서는 두 히어로, Black LionPrincess Sure-E를 먼저 만들고, 그 다음 heroes 인자에 전달했어요.

앞과 마찬가지로 Team 인스턴스만 세션에 추가하면 되고, 히어로들이 그와 연결되어 있으므로 커밋할 때 그들도 자동으로 저장된다는 점을 눈여겨보세요.

다(Many) 쪽에서 관계 객체 포함하기

앞에서 이것이 다대일(many-to-one) 관계라고 했죠. 한 팀에 여러 히어로가 속할 수 있으니까요.

이 관계 속성들을 다(多) 쪽에서도 데이터를 연결하는 데 사용할 수 있어요.

team.heroes 속성은 리스트처럼 동작하므로, 그냥 append하면 돼요.

히어로들을 몇 개 더 만들고 team_preventers.heroes 리스트 속성에 추가해 볼게요:

# Code above omitted 👆

def create_heroes():
    with Session(engine) as session:

# Code here omitted 👈

        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)

# Code below omitted 👇

team_preventers.heroes 속성은 리스트처럼 동작해요. 하지만 특별한 종류의 리스트인데, 히어로들을 추가해서 수정하면 SQLModel(정확히는 SQLAlchemy)이 데이터베이스에서 해야 할 필요한 변경 사항을 추적하기 때문이에요.

그런 다음 팀을 add()하고 commit() 해요.

그리고 앞과 똑같이, 히어로들은 팀에 연결되어 있으므로 개별 히어로들을 세션에 add() 할 필요조차 없어요.

정리

이 관계 속성들을 사용하면 평범한 Python 객체와 속성으로 데이터 연결을 만들고 업데이트할 수 있어요. 😎

다음에는 이 관계 속성들로 연결된 데이터를 읽는 방법을 살펴볼게요. 🤝

더 알아보기 (Learn more)