연결된 행 만들기

연결된 행 만들기 (Create and Connect Rows)

이번 장에서는 각 테이블의 행을 만들고, 행끼리 연결하는 방법을 배워요. 팀을 만들고, 히어로를 만들면서 히어로가 팀을 가리키도록 하는 거예요.

출처: 공식문서

현재 데이터 상태

team 테이블은 이렇게 생길 거예요.

id name headquarters
1 Preventers Sharp Tower
2 Z-Force Sister Margaret's Bar

이 장의 작업을 마치면 hero 테이블은 이렇게 생깁니다.

id name secret_name age team_id
1 Deadpond Dive Wilson null 2
2 Rusty-Man Tommy Sharp 48 1
3 Spider-Boy Pedro Parqueador null null

hero 테이블의 각 행은 team 테이블의 행을 가리켜요.

table relationships

참고

나중에 Spider-BoyPreventers 팀에도 추가하긴 할 거지만, 지금은 아직이에요.

이전 예제의 코드를 이어서, 거기에 더 많은 것들을 추가할 거예요.

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 main():
    create_db_and_tables()


if __name__ == "__main__":
    main()

예제를 실행하기 전에 database.db 파일을 꼭 삭제해서 같은 결과를 얻도록 하세요.

SQLModel로 팀 행 만들기

전에 했던 것처럼 create_heroes() 함수를 정의해서 우리 히어로들을 만들고, 이제는 그 안에서 팀들도 만들 거예요. 🎉

두 개의 팀을 만드는 것부터 시작할게요.

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

# Code below omitted 👇

이건 아마 이미 익숙할 거예요.

위에서 만든 같은 engine을 사용해 with 블록 안에서 session을 시작하고, 모델 클래스(여기선 Team)의 인스턴스 두 개를 만든 다음, 그 객체들을 session에 추가하고, 마지막으로 commit 해서 변경 사항을 데이터베이스에 저장해요.

main()에 추가하기

명령줄에서 프로그램을 호출할 때 이 함수가 실행되도록, 이 create_heroes() 함수를 main() 함수에 추가하는 걸 잊지 말아요.

# Code above omitted 👆

def main():
    create_db_and_tables()
    create_heroes()

# Code below omitted 👇

실행하기

지금까지의 코드를 실행하면 이렇게 출력돼요.

$ uv run python app.py

// Previous output omitted 😉

// Automatically start a transaction
INFO Engine BEGIN (implicit)
// Add the teams to the database
INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?)
INFO Engine [generated in 0.00050s] ('Preventers', 'Sharp Tower')
INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?)
INFO Engine [cached since 0.002324s ago] ('Z-Force', 'Sister Margaret's Bar')
INFO Engine COMMIT

출력에서 보듯이, 행을 만들 때 일반적인 SQL INSERT 문을 사용해요.

코드로 히어로 행 만들기

이제 히어로 객체 하나를 만들어 볼게요.

Hero 클래스 모델에 이제 team_id라는 필드(컬럼, 속성)가 있으니, 방금 만든 Team 객체들의 ID 필드를 사용해 그 값을 설정할 수 있어요.

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

# Code below omitted 👇

이 히어로를 아직 데이터베이스에 commit 하지는 않았는데, 여기서 이미 주의할 점이 몇 가지 있어요.

만약 데이터베이스에 이미 팀들이 있었다면, 데이터베이스가 각 팀에 자동으로 부여할 ID가 무엇인지 우리가 알 수 없을 거예요. 그냥 1이나 2라고 추측할 수는 없죠.

하지만 팀이 만들어져 데이터베이스에 commit 되면, 객체의 id 필드에 접근해 그 ID를 얻을 수 있어요.

방금 commit 된 모델의 속성에, 예를 들어 team_z_force.id처럼 접근하면, 자동으로 데이터베이스에서 객체의 데이터를 refresh 하는 동작이 촉발되고, 그 필드의 값을 노출해줘요.

그래서 이 히어로를 아직 commit 하지 않았더라도, team_z_force.id를 쓰는 것만으로 이 팀의 데이터를 가져오는 SQL이 데이터베이스로 전송될 거예요.

그 코드 한 줄만으로도 이렇게 출력이 생겨요.

INFO Engine BEGIN (implicit)
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.00025s] (2,)

이제 히어로 두 명을 더 만들어 볼게요.

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

# Code below omitted 👇

hero_rusty_man을 만들 때 team_preventers.id에 접근하니까, 이것도 그 데이터를 refresh 하게 되고 이런 출력이 생겨요.

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 [cached since 0.001795s ago] (1,)

또 하나 눈여겨볼 점이 있어요. 우리는 team_idint | None으로 표시했는데, 이는 데이터베이스에서 NULL(파이썬에서는 None)이 될 수 있다는 뜻이에요.

즉, 히어로가 꼭 팀을 가질 필요는 없어요. 여기서 Spider-Boy는 팀이 없죠.

다음으로 변경 사항을 데이터베이스에 저장하기 위해 commit 하면 이런 출력이 생겨요.

INFO Engine INSERT INTO hero (name, secret_name, age, team_id) VALUES (?, ?, ?, ?)
INFO Engine [generated in 0.00022s] ('Deadpond', 'Dive Wilson', None, 2)
INFO Engine INSERT INTO hero (name, secret_name, age, team_id) VALUES (?, ?, ?, ?)
INFO Engine [cached since 0.0007987s ago] ('Rusty-Man', 'Tommy Sharp', 48, 1)
INFO Engine INSERT INTO hero (name, secret_name, age, team_id) VALUES (?, ?, ?, ?)
INFO Engine [cached since 0.001095s ago] ('Spider-Boy', 'Pedro Parqueador', None, None)
INFO Engine COMMIT

히어로 refresh 하고 출력하기

이제 새 히어로들을 refresh 하고 출력해서, 팀을 가리키는 새 ID를 확인해 볼게요.

# Code above omitted 👆

def create_heroes():
    with Session(engine) as session:
        # Code here omitted 👈

        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 👇

명령줄에서 실행하면 이렇게 출력돼요.

$ uv run python app.py

// Previous output omitted 😉

// Automatically start a transaction
INFO Engine BEGIN (implicit)

// Refresh the first hero
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00021s] (1,)
// Refresh the second hero
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.001575s ago] (2,)
// Refresh the third hero
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.002518s ago] (3,)

// Print the heroes
Created hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None
Created hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48
Created hero: id=3 secret_name='Pedro Parqueador' team_id=None name='Spider-Boy' age=None

이제 히어로들이 자기 team_id를 갖게 됐네요. 좋아요!

관계(Relationships)란

SQL 데이터베이스에서의 관계는, 그저 한 테이블의 컬럼다른 테이블의 컬럼 값을 참조하는 것으로 만들어져요.

그리고 여기서 우리는 정확히 그렇게 다뤘어요. 좀 더 많은 컬럼 필드로요. 실제로 SQL 데이터베이스 속에서는 그게 전부예요.

하지만 이 튜토리얼의 다음 장들, Relationship Attributes에서 코드로 훨씬 더 다루기 쉽게 만드는 방법을 배우게 될 거예요. ✨

더 알아보기 (Learn more)