본 시리즈는 '박응용' 님의 '점프 투 FastAPI'를 바탕으로 학습 및 실습한 내용을 정리한 것입니다.
구현 및 파인튜닝한 모델을 사용한 웹 서비스 구현을 위해 FastAPI의 학습 필요성을 느껴 학습 과정을 정리합니다. 내용의 정확성이나 이론적인 부분은 당연히 원본 페이지를 참조하시는 게 좋고, 본 시리즈에서는 구현 도중 발생하는 문제 등을 해결하는 과정을 함께 기록하여 '처음부터 끝까지 따라 할 수 있는' 시리즈를 만드는 것을 목표로 합니다(물론 제1목표는 학습 내용 기록입니다).
이번 포스트에서는 앞서 만든 데이터베이스에 대하여 데이터를 생성(Create), 조회(Read), 수정(Update), 삭제(Delete) - CRUD - 하는 방법에 대해 알아본다.
모델을 테스트하기 위해 파이썬 셸을 이용한다. 파이썬 셸은 가상환경에서 실행한다.
(practice_1) C:\workspace\fastapi_practice\practice_1> python
다음과 같이 입력해 Question 모델 객체를 하나 생성하자.
>>> from models import Question, Answer
>>> from datetime import datetime
>>> q = Question(subject="FastAPI는 무엇인가요?", content="FastAPI의 기능과 특징에 대해 알고 싶습니다.", create_date=datetime.now())
Question 모델의 속성 중 create_date는 데이터 타입이 DateTime이므로 datetime.now()를 이용해 현재 시각을 대입한다.
이렇게 객체 q를 만들었다고 해서 데이터에 해당 데이터가 바로 저장되는 것은 아니다. database.py를 만들 때 autocommit 인자를 False로 설정했기 때문이다. 따라서 database.py 파일의 SessionLocal 클래스로 db 세션 객체를 생성하여 사용해야 한다.
>>> from database import SessionLocal
>>> db = SessionLocal()
>>> db.add(q)
>>> db.commit()
이렇게 커밋을 함으로써 데이터베이스에 데이터가 저장된다. 데이터가 잘 생성되었는지 확인해보자.
>>> q.id
1
Question 테이블에 대하여 최초로 생성한 인스턴스이며, 속성 중 id는 PK(Primary Key)이기 때문에 1부터 시작해 자동으로 증가하는 방식으로 생성된다. 두 번째 질문 데이터를 생성해보면 이를 확인할 수 있다.
>>> q = Question(subject="FastAPI 모델의 id에 대해 질문합니다.", content="FastAPI의 id 속성은 자동으로 생성되는 것인가요?", create_date=datetime.now
())
>>> db.add(q)
>>> db.commit()
>>> q.id
2
결과는 예상대로 2가 출력되는 것을 확인할 수 있다.
이번엔 데이터베이스에 저장된 데이터를 조회해 보자.
>>> db.query(Question).all()
[<models.Question object at 0x0000020D73AC35D0>, <models.Question object at 0x0000020D73A74CD0>]
filter 함수를 이용해 첫 번째 질문만 조회해 보자.
>>> db.query(Question).filter(Question.id==1).all()
[<models.Question object at 0x0000020D73AC35D0>]
전체 조회 결과 화면과 비교해 보면 첫 번째 데이터와 메모리 주소가 동일하다. 즉, 첫 번째 질문 데이터만 조회했음을 알 수 있다.
이때 id는 유일한 값이므로(PK는 해당 인스턴스를 식별할 수 있도록 유일성을 가지고 있다) get 함수를 이용해서도 조회할 수 있다.
>>> db.query(Question).get(1)
단, 이 방법은 SQLAlchemy 2.0 이후부터 레거시로 간주되며, Session.get()으로 해당 기능을 사용할 수 있으므로 참고하자.
이번에는 filter와 like를 이용해 content에 "id"라는 문자열이 포함된 질문을 조회해 보자. 예상대로라면 2번째 질문 데이터가 조회되어야 할 것이다.
>>> db.query(Question).filter(Question.content.like('%id%')).all()
[<models.Question object at 0x0000020D73A74CD0>]
예상대로 두 번째 질문에 해당하는 데이터가 조회되었다. 만약 대소문자를 구별하지 않으려면 like 대신 ilike 함수를 사용하면 된다.
SQLAlchemy 공식 문서를 통해 다양한 데이터 조회 방법을 확인할 수 있다.
데이터를 수정할 때는 python의 기본인 대입 연산자를 이용하면 된다.
>>> q = db.query(Question).get(2)
>>> q.id
2
>>> q.subject = "FastAPI Model Question."
>>> db.commit()
첫 번째 질문을 삭제해 보자.
>>> q = db.query(Question).filter(Question.id==1).all()[0]
>>> db.delete(q)
>>> db.commit()
삭제도 마찬가지로 commit()을 해 주어야 데이터베이스에 반영된다. 이제 실제 DB에서 첫 번째 질문이 삭제되었는지 확인해보자.
db.query(Question).all()
[<models.Question object at 0x0000020D73AD4E50>]
데이터가 하나 삭제되어 하나의 질문 데이터만 남아 있음을 확인할 수 있다.
이번에는 답변(Answer) 테이블에 데이터를 생성하고 저장해 보자.
>>> q = db.query(Question).filter(Question.id==2).all()[0]
>>> a = Answer(question=q, content="네, 자동으로 생성됩니다.", create_date=datetime.now())
>>> db.add(a)
>>> db.commit()
답변을 생성하려면 어떤 질문인지 확인해야 하므로 질문을 먼저 조회한다. id가 2인 질문을 조회하여 변수 q에 저장하고, Answer 모델의 question 속성에 방금 저장한 q 객체를 대입하여 답변 데이터를 생성한다.
이렇게 하면 Answer 모델의 속성 중 질문과 답변 데이터를 연결해 주기 위한 question_id 속성에 별도로 값을 지정하지 않아도 자동으로 입력되어 저장되며, 따라서 question_id에 별도로 값을 설정할 필요가 없다.
Answer 역시 Question과 마찬가지로 id를 PK로 설정해 두었으므로 자동으로 값이 생성된다.
>>> a.id
1
>>> a = db.query(Answer).filter(Answer.id==1).all()[0]
>>> a
<models.Answer object at 0x0000020D73AD6310>
질문 데이터의 id는 알고 있지만 답변 데이터의 id는 모를 경우, 당연하게도 이를 이용해 답변 데이터를 조회할 수도 있다.
>>> a = db.query(Answer).filter(Answer.question_id==2).all()[0]
>>> a
<models.Answer object at 0x0000020D73AD6310>
답변에 연결된 질문 찾기의 경우 Answer 모델의 question 속성에 질문이 저장되어 있으므로 매우 쉽다.
>>> a.question
<models.Question object at 0x0000020D73AD4E50>
거꾸로 질문에 달린 답변을 찾는 것은? Answer 모델의 question 속성 중 역참조 설정(backref=('answers'))이 적용되어 있으므로 이를 이용하면 쉽게 가능하다.
>>>q.answers
[<models.Answer object at 0x0000020D73AD6310>]