포스트

[DB] SQLModel로 데이터베이스 다루기 <4>

공식문서 참조

1. 연관된 테이블 만들기

지금까지는 단 하나의 테이블에서만 데이터를 불러왔다. 하지만 서로 연관없는 데이터들을 분리하기 위해 테이블을 여러개로 나누는 경우가 많다. 예를 들어보자

Hero table

idnamesecret_nameageteam_nameheadquartersteam_id
1IronmanStark40AvangersLA1
2C.AmericaRogers100AvangersLA1
3SpidermanParker17AvangersLA1
4BatmanWayne41J.LeagueCalifornia2
5SupermanKent500J.LeagueCalifornia2

이 Hero 테이블에서 속해있는 히어로 팀 데이터는 중복되고 있는 것을 볼 수 있다. 또한 해당 데이터들은 팀에 대한 데이터지, 히어로와는 별 관련이 없다. 이런 데이터들을 아래와 같이 분리할 수 있다.

Hero table

id (Primary Key)namesecret_nameageteam_id (Foreign Key = team.id)
1IronmanStark401
2C.AmericaRogers1001
3SpidermanParker171
4BatmanWayne412
5SupermanKent5002

Team table

id (Primary Key)nameheadquarters
1AvangersLA
2J.LeagueCalifornia

한눈에 보기 쉬워졌다. 이런 데이터를 SQLModel로 만드는 방법을 알아보자

1.1 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sqlmodel import Field, SQLModel

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 = Feild(index = True)
    secret_name: str
    age: int | None = Field(default=None, index=True)
    team_id: int | None = Field(default=None, foreign_key="team.id")

이때 외래키 foreign_key는 문자열로 지정해주고, 대문자가 아닌 소문자 team.id로 해준다.

2. 연관된 테이블에 데이터 넣기

POST를 통해 테이블에 데이터를 넣어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.post("/", status_code=status.HTTP_201_CREATED)
async def create_heros(session:Annotated[AsyncSession, Depends(get_async_session)]):
    team1 = Team(name="Avangers", headquarters="LA")
    team2 = Team(name="J.League", headquarters="California")

    async with session.begin():
        session.add_all([team1, team2])

    hero_1 = Hero(name="Ironman", secret_name="Stark", age=40, team_id=team1.id)
    hero_2 = Hero(name="C.America", secret_name="Rogers", age=100, team_id=team1.id)
    hero_3 = Hero(name="Spiderman", secret_name="Parker", age=17, team_id=team1.id)
    hero_4 = Hero(name="Batman", secret_name="Wayne", age=41, team_id=team2.id)
    hero_5 = Hero(name="Superman", secret_name="Kent", age=500, team_id=team2.id)

    async with session.begin():
        session.add_all([hero_1, hero_2, hero_3, hero_4, hero_5])

    return {"msg": "Heros created"}

이때 외래에서 불러올 키가 반드시 DB에 먼저 존재해야 하는듯 싶다. 아래와 같이 한번에 커밋하려하면 team_id 가 Null 값으로 들어간다.

1
2
3
4
## team_id가 Null로 들어가는 코드. 이렇게 하면 안됨
async with session.begin():
	session.add_all([team1, team2])
    session.add_all([hero_1, hero_2, hero_3, hero_4, hero_5])

값이 제대로 들어간 것을 볼 수 있다.

3. 연관된 테이블에서 데이터 조회 JOIN

테이블을 나누어 중복된 데이터를 제거한것 까지는 좋다. 하지만 이런 경우라면 어떨까?

3.1 단일 조회

“나는 spiderman이 어떤 히어로 팀에 속했는지 알고 싶어”

Hero 테이블만 봤을 때는 팀의 id만 알 수 있다. 그 id가 어느 팀을 가리키는지는 알지 못한다.

Team 테이블만 봤을 때는 히어로가 어디에 속했는지 알 수 없다.

즉 동시에 두 테이블을 한번에 조회해야 한다. 이때 사용하는 메서드가 join() 이다.

1
2
3
4
5
6
7
@app.get("/", status_code=status.HTTP_200_OK)
async def get_heros(session:Annotated[AsyncSession, Depends(get_async_session)]):
    statement = select(Team).join(Hero).where(Hero.name == "Spiderman")
    results = await session.exec(statement)
    hero = results.one_or_none()
    
    return hero

select 로 조회할 테이블을 고른 후,

join 으로 조건을 설정할 테이블을 고른다.

그 이후 wherejoin 에서 고른 테이블의 열을 지정해 준다.

Spiderman 이 어떤 히어로 팀에 속했는지 조회됐다.

3.2 여러개 조회

여러 데이터를 조회하는 것도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.get("/", status_code=status.HTTP_200_OK)
async def get_heros(session:Annotated[AsyncSession, Depends(get_async_session)]):
    statement = select(Hero, Team).join(Team)
    results = await session.exec(statement)
    heros = results.fetchall()
        
    response = []
    for hero, team in heros:
        response.append({
            "hero": hero.model_dump(),  # SQLModel 객체를 dict로 변환
            "team": team.model_dump()
        })
    
    return response

이때 주의점. select 를 통해 2개 이상의 테이블을 조회할 때, heros를 반드시 dict로 직렬화 해주어야 한다.

단일 테이블의 데이터의 경우 이미 SQLModel 로 선언한 모델이라 자동으로 직렬화되어 출력되지만, 두개를 붙인 경우 tuple의 형태가 되기 때문이다.

이 경우엔 특별한 조건 없이 두 데이터를 조회했다. 두 테이블은 이미 team_id로 묶여있기 때문에 가능하다.

두 테이블이 합쳐져서 조회된다.



이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.