연결된 행 만들기
연결된 행 만들기 (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 테이블의 행을 가리켜요.
참고
나중에 Spider-Boy를 Preventers 팀에도 추가하긴 할 거지만, 지금은 아직이에요.
이전 예제의 코드를 이어서, 거기에 더 많은 것들을 추가할 거예요.
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_id를 int | 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에서 코드로 훨씬 더 다루기 쉽게 만드는 방법을 배우게 될 거예요. ✨