포스트

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

공식문서 참조

1. UPDATE

앞서서 데이터를 만들고 조회하는 것 까지 알아보았다. 이제 만들어진 데이터를 수정해보자.

SQLModel 에서는 따로 수정하기 위한 메서드는 있지 않는 듯 하다. 대신 앞서서 데이터를 조회한 후, 해당 객체의 값을 직접 수정해주면 된다.

아래는 공식 문서에서 제공하는 코드이다.

1
2
3
4
5
6
7
8
def update_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Spider-Boy")
        results = session.exec(statement)
        hero = results.one()
        print("Hero:", hero)

        hero.age = 16

보다시피 hero 객체의 age에 직접 접근해 수정해주고 있다.

하지만 값을 직접 수정하는 것은 큰 프로젝트에선 오류가 날 가능성이 높기 때문에 수정 전용 클래스를 따로 만드는 것이 안전하다.

1.1 수정 전용 클래스

1
2
3
4
class HeroUpdate(SQLModel):
    name: Optional[str] = None
    secret_name: Optional[str] = None
    age: Optional[int] = None

Update를 위한 전용 클래스이다. 이때 id는 대부분의 경우 수정하지 않을 것이기 때문에 제외한다.

또한 수정 시에 각 항목을 모두 업데이트 하지 않고, 원하는 항목만 업데이트 할 수 있게 Optional 로 만들어준다. Optional 안에서도 타입 체크는 가능하다.

주의: 이때 None을 붙여주지 않으면 에러난다.

1.2 Patch

FastAPI 에선 patch 를 통해 Update를 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@app.patch("/{word}", status_code=status.HTTP_200_OK)
async def update_heros(
    session:Annotated[AsyncSession, Depends(get_async_session)],
    update_hero: HeroUpdate,
    word: str
    ):

    async with session.begin():

        statement = select(Hero).where(Hero.name == word)
        result = await session.exec(statement)
        hero = result.one_or_none()

        if hero is None:
            raise HTTPException(status_code=404, detail = "Hero not exist")

        if update_hero.name is not None:
            hero.name = update_hero.name
        if update_hero.secret_name is not None:
            hero.secret_name = update_hero.secret_name
        if update_hero.age is not None:
            hero.age = update_hero.age
        
        session.add(hero)
    
    return hero

각각의 항목이 존재하지 않을 수 있기 때문에 예외 처리를 확실하게 해준다.

hero_update 는 프론트의 바디에서 받아온다.

값이 제대로 바뀐 것을 볼 수 있다.

2. DELETE

이제 db에 들어가있는 데이터를 삭제해보자.

아래는 공식 문서에서 제공하는 delete 예제다.

1
2
3
4
5
6
7
8
9
def delete_heroes():
    with Session(engine) as session:
        statement = select(Hero).where(Hero.name == "Spider-Youngster")
        results = session.exec(statement)
        hero = results.one()
        print("Hero: ", hero)

        session.delete(hero)
        session.commit()
  1. 원하는 행을 select한다.
  2. 삭제한다.

간단하다.

이제 FastAPI에서 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.delete("/{word}", status_code=status.HTTP_200_OK)
async def delete_hero(
    session:Annotated[AsyncSession, Depends(get_async_session)],
    word: str
):
    async with session.begin():
        statement = select(Hero).where(Hero.name == word)
        result = await session.exec(statement)
        hero = result.one_or_none()

        await session.delete(hero)
    
    return hero

만약 눈치가 빠르다면 뭔가 다른점을 느꼈을 것이다.

2.2 DELETE 에만 await가 사용되는 이유

앞에서 session 객체에 사용했던 메서드 중, 유일하게 delete 만이 await를 요구한다. 만약 add 라면 다음과 같이 작동한다.

  1. session.add(hero1) : session에 hero1 데이터를 추가하도록 예약을 넣는다.
  2. await session.commit() : 커밋을 하는 시점에 db에 적용된다.

이것이 가능한 이유는, add 메서드가 내부적으로 db와 통신하는 과정이 없기 때문에 비동기 처리가 필요 없는 것이다.

하지만 delete 는 일부의 경우에 데이터베이스와 직접적인 상호작용이 있을 수 있어서 반드시 await를 요구한다.

자세하게 어떤 과정일까? 이유는 다음과 같다.

  1. await session.delete() 역시 add와 마찬가지로 세션 내에서 삭제 처리(예약)을 함.
  2. delete도 커밋 이전에는 db에 반영되지 않음.
  3. DELETE의 경우, 다른 테이블과의 상관관계(CASCADE) 가 있을 수 있다.
  4. 해당 상관관계를 DB에서 불러와 추가적인 삭제 쿼리가 필요할 수 있기 때문에 DB와 상호작용이 필요하다.
  5. 해당 연관 객체들을 비동기적으로 불러와야 하기 때문에 await를 사용한다.

실제로 비동기적 과정에서 await 없이 delete를 사용할 경우 CASCADE가 없어도 db에 반영되지 않았다.

원하는 히어로가 삭제된 것을 볼 수 있다.



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