프레임워크/FastAPI

[FastAPI] FastAPI [24] SQL (Relational) Databases (1)

:) :) 2023. 5. 30. 14:48

https://fastapi.tiangolo.com/tutorial/sql-databases/

 

SQL (Relational) Databases - FastAPI

SQL (Relational) Databases FastAPI doesn't require you to use a SQL (relational) database. But you can use any relational database that you want. Here we'll see an example using SQLAlchemy. You can easily adapt it to any database supported by SQLAlchemy, l

fastapi.tiangolo.com

<FastAPI 공식문서 참조>

 

* 공식문서 순서상으론 Dependencies(의존성), Security(보안) 다음 SQL Databases이나 필요에 의해 본 Chapter부터 포스팅합니다.

 

 

 

0. SQL (Relational) Databases

 FastAPI 사용에 SQL 데이터베이스가 필수적이진 않지만 원하면 사용할 수 있다.

 

Python에서 SQL의 사용은 다음 페이지를 참조하면 좋을 것 같다.

https://www.sqlalchemy.org/
 

SQLAlchemy

The Database Toolkit for Python

www.sqlalchemy.org

 

SQLAlchemy에서 지원하는 모든 데이터베이스를 쉽게 적용할 수 있다.

  • PostgreSQL
  • MySQL
  • SQLite
  • Oracle
  • Microsoft SQL Server
  • etc.

등이 사용 가능하다.

 

 본 포스팅에서는 SQLite를 사용할 것이다. 이는 하나의 파일로써 사용가능하며 Python과 통합되어있기 때문이다.

즉, 다음 예제들을 쉽게 복사-붙여넣기해서 실행할 수 있다.

 

추후에, 내 어플리케이션 개발을 위해 PostgreSQL같은 Database Server를 사용하면 된다.

 

 

*다음 URL은 FastAPI와 PostgreSQL을 사용한 공식 프로젝트 생성기이다. 모두 Docker 기반이다.

https://github.com/tiangolo/full-stack-fastapi-postgresql
 

GitHub - tiangolo/full-stack-fastapi-postgresql: Full stack, modern web application generator. Using FastAPI, PostgreSQL as data

Full stack, modern web application generator. Using FastAPI, PostgreSQL as database, Docker, automatic HTTPS and more. - GitHub - tiangolo/full-stack-fastapi-postgresql: Full stack, modern web appl...

github.com

 

 

1. ORMs

 Database와 상호작용하기 위한 일반적 패턴은 ORM(object-relational mapping)(객체지향 매핑)을 사용한다.

Object(객체)와 database Table( relation이라 불리는 )을 서로 convert( map 이라 불리는)하는 도구를 가지고 있다.

 

 다음 예시를 보자.

아래 Pet class는 SQL table pets 를 의미한다.

그리고 각각의 인스턴스객체는 테이블 내 튜플, 하나의 행을 의미한다.

 

예시로, Pet의 인스턴스인 orion_cat object는 type 속성에 대한 값으로 cat을 가지고 있고,

이를 orion_cat.type 으로 접근 가능하다.

 

이러한 방식으로, orion_cat.owner 속성을 가져올 수 있고 이는 pet의 주인의 데이터를 포함하고 있다.

쉽게 orion_cat.owner.name의 의미를 유추해 볼 수 있다. 그만큼 직관적이다.

 

 

SQLAlchemy상의 ORM으로 구현한 예시를 보자.

 

 

2. File structure

my_super_project 디렉토리를 하나 만들자.

그 속엔 다음 구조를 가지는 sql_app 서브 디렉토리가 있다.

.
└── sql_app
    ├── __init__.py
    ├── crud.py
    ├── database.py
    ├── main.py
    ├── models.py
    └── schemas.py

* __init__.py는 반드시 빈 파일이어야 한다. 이는 sql_app과 모든 모듈들이 패키지임을 의미한다.

 

각 모듈(python file)이 어떤 역할을 하는지 보자.

 

3. Install SQLAlchemy

pip install sqlalchemy

SQLAlchemy를 설치하자.

 

 

4. Create the SQLAlchemy parts

* sql_app/database.py 파일에서 작업

 

본 chapter의 전체 코드는 아래와 같다.

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

 

4-1. Import the SQLAlchemy parts

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

 

4-2. Create a database URL for SQLAlchemy

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"

 SQLite 데이터베이스와 연결하는 과정이다.

그 파일(데이터베이스)은 sql_app.db 디렉토리 속에 위치해있어야 한다.

그래서 URL 마지막 부분이  ./sql_app.db.  이다.

 

 주석은 PostgreSQL database와 연결하는 코드를 나타내고 있다.

 

4-3. Create the SQLAlchemy engine

연결 이후, 첫 단추는 "engine 생성"으로 꿰맨다.

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

 

아래 코드는 SQLite에서만 쓰고, 나머지는 필요없다.

connect_args={"check_same_thread": False}

이와 연관된 Technical Details는 다음과 같다.

더보기

Technical Details

By default SQLite will only allow one thread to communicate with it, assuming that each thread would handle an independent request.

This is to prevent accidentally sharing the same connection for different things (for different requests).

But in FastAPI, using normal functions (def) more than one thread could interact with the database for the same request, so we need to make SQLite know that it should allow that with connect_args={"check_same_thread": False}.

Also, we will make sure each request gets its own database connection session in a dependency, so there's no need for that default mechanism.

 

4-4. Create a SessionLocal class

 SessionLocal class의 각 인스턴스는 Database session이 된다(클래스 자체는 데이터베이스 세션이 아직 아님).

이것에 인스턴스 생성시 그제야 데이터베이스 세션이 된다.

 

이후에 SQLAlchemy에서 import 할 Session과 구분하기 위해 SessionLocal로 이름지었다.

 

sessionmaker 함수로 SessionLocal을 생성한다.

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

 

4-5. Create a Base class

 나중에 상속하면서 두고두고 써먹을 Base class를 선언하는 과정이다.

Base = declarative_base()

 

 

5. Create the database models

 이제 데이터베이스 모델을 생성해보자.

sql_app/models.py 파일에서 구현한다.

 

Chapter 5의 전체 코드는 다음과 같다.

from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .database import Base


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)

    items = relationship("Item", back_populates="owner")


class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, index=True)
    owner_id = Column(Integer, ForeignKey("users.id"))

    owner = relationship("User", back_populates="items")

 

5-1. Create SQLAlchemy models from the Base class

 SQLAlchemy model에서 만든 base 클래스를 상속해서 사용하자.

from .database import Base


class User(Base):
    __tablename__ = "users"


class Item(Base):
    __tablename__ = "items"

 __tablename__ 속성은 각 모델에 대해 데이터베이스에서 사용할 테이블의 이름을 의미한다.

 

 

5-2. Create model attributes/columns

from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .database import Base


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)

    items = relationship("Item", back_populates="owner")


class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, index=True)
    owner_id = Column(Integer, ForeignKey("users.id"))

    owner = relationship("User", back_populates="items")

 이 속성들은 데이터베이스의 열을 의미한다.

열을 의미하기 위해 Column 을 기본으로 사용한다.

각 요소마다(Column함수의 parameter로 주어지는) 주 키 및 여러 제약조건 설정이 가능하다.

 

5-3. Create the relationships

from sqlalchemy.orm import relationship

테이블관 관계 설정을 위해 패키지를 import 한다.

 

 

다음을 보자.

    items = relationship("Item", back_populates="owner")

User 속 items에 접근할 때(my_user.items 같이 접근할 때)

이는 items table에서 Item 모델의 값들을 가져온다. 이 때 외래 키가 있는 속성에서 가져온다.

 

다음을 보자.

    owner = relationship("User", back_populates="items")

Item 속 owner 속성에 접근할 때, users table의 User model이 포함된다. user table에서 어떤 레코드를 가져왔는지 알기 위해 외래 키와 함께 owner_id 속성을 사용한다.

 

 

6. Create the Pydantic models

 다음은 sql_app/schemas.py 파일에서 구현한다. 

 

 SQLAlchemy model과 Pydantic model을 구분하기 위해

 

SQLAlchemy model은 models.py 파일 속에,

Pydantic model은 schemas.py 파일 속에 구현한다.

 

6-1. Create inital Pydantic models / schemas

 기본 Base Pydantic model을 생성한다(스키마라고 하자).

이는 생성과정 혹은 읽는과정에서 공통된 속성을 가져야 한다(변하면 안된다).

 

그리고 각 Base model을 상속한 ItemCreate 와 UserCreate를 생성한다(그렇게 공통된 속성을 가지도록 만들 수 있다)(속성 추가도 가능).

from pydantic import BaseModel


class ItemBase(BaseModel):
    title: str
    description: str | None = None


class ItemCreate(ItemBase):
    pass


class Item(ItemBase):
    id: int
    owner_id: int

    class Config:
        orm_mode = True


class UserBase(BaseModel):
    email: str


class UserCreate(UserBase):
    password: str


class User(UserBase):
    id: int
    is_active: bool
    items: list[Item] = []

    class Config:
        orm_mode = True

여러 테이블들을 간단하게 만들었다.

 

*SQLAlchemy와 Pydantic model에는 속성(Attribute)을 정의하는 표기법에 차이가 있다.

 

SQLAlchemy style

name = Column(String)

 

Pydantic style

name: str

 

헷갈리지 말자.

 

 

6-2. Create Pydantic models / schemas for reading / returning

 API에서 사용할 위 메소드들을 만들어보자.

 

class Item(ItemBase):
    id: int
    owner_id: int
class User(UserBase):
    id: int
    is_active: bool
    items: list[Item] = []

Item을 만들기 전에는 어떤 id가 할당될지 모르는 상태이지만

User를 읽을 때는 user에 속한 item에 접근할 수 있도록 items를 선언할 수 있다.

 

6-3. Use Pydantic's orm_mode

 이 reading 메소드들을 위해 내부 Config class를 더해준다.

이는 Pydantic에 세부 구성들을 제공하기 위해 사용된다.

orm_mode = True로 설정하자. 그럼 Pydantic model이 데이터가 dict 형태가 아니더라도 읽을 수 있도록 하게 해준다.

 

class Item(ItemBase):
    id: int
    owner_id: int

    class Config:
        orm_mode = True
class User(UserBase):
    id: int
    is_active: bool
    items: list[Item] = []

    class Config:
        orm_mode = True

 

 

원래 다음과 같은 형태만 읽는데,

id = data["id"]

 

아래같은 문법도 읽을 수 있게 해준다.

id = data.id

 

이 configuration은 ORM model 과 Pydantic model을 비교가능하게 만들어 주어

 

path operation속 response_model의 argument로도 ORM model을 사용 가능하도록 해준다.

 

 

7. CRUD utils

 sql_app/crud.py.  파일을 보자.

이 파일에 데이터베이스와 상호작용 가능한 여러 함수들이 존재한다. 이 함수들은 당연히 재사용가능하다.

그들 중 몇 가지만 소개하는데, 그 이름하야

Create, Read, Update, Delete, CRUD 이다.

 

7-1. Read data

 여러 유틸리티 함수들을 만든다. 아래의 예시는 각각

  • ID에 의해 식별되는 하나의 사용자를 읽음
  • Email에 의해 식별되는 하나의사용자를 읽음
  • 여러 사용자를 읽음
  • 여러 item을 읽음

을 의미한다.

 

그 전에 각 기능을 위한 여러 패키지를 import 한다.

from sqlalchemy.orm import Session

from . import models, schemas

 

def get_user(db: Session, user_id: int):
    return db.query(models.User).filter(models.User.id == user_id).first()


def get_user_by_email(db: Session, email: str):
    return db.query(models.User).filter(models.User.email == email).first()


def get_users(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.User).offset(skip).limit(limit).all()
    
    
def get_items(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.Item).offset(skip).limit(limit).all()

 

7-2. Create data

 이번엔 데이터 생성을 위한 유틸리티 함수를 만들어보자.

 

과정은 다음과 같다.

  • 내 데이터에 SQLAlchemy 모델 인스턴스를 생성한다.
  • 이를 내 데이터베이스 세션에 삽입한다.
  • 데이터베이스상의 변화를 commit한다.
  • 인스턴스를 refresh(새로고침) 한다(그래야 수정사항을 포함할 수 있다).
def create_user(db: Session, user: schemas.UserCreate):
    fake_hashed_password = user.password + "notreallyhashed"
    db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user
def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
    db_item = models.Item(**item.dict(), owner_id=user_id)
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

 

 두 함수 모두 생성-삽입-커밋-새로고침의 과정을 따르고 있다.

 

이후에 계속

 

 

 

8. Reference

https://fastapi.tiangolo.com/tutorial/sql-databases/

 

SQL (Relational) Databases - FastAPI

SQL (Relational) Databases FastAPI doesn't require you to use a SQL (relational) database. But you can use any relational database that you want. Here we'll see an example using SQLAlchemy. You can easily adapt it to any database supported by SQLAlchemy, l

fastapi.tiangolo.com