Core
- 데이터베이스 도구 키트로, SQLAlchemy의 기본 아키텍쳐
- 데이터베이스에 대한 연결을 관리하고, 데이터베이스 쿼리 및 결과와 상호작용하고 SQL문을 프로그래밍 방식으로 구성하기 위한 도구를 제공합니다.
ORM(Object Relational Mapper)
- Core를 기반으로 구축되어 선택적 ORM 기능을 제공합니다.
1. sqlalchemy class (object) : db table
2. sqlalchemy class attribute : db column
3. sqlalchemy class instance : db row
DATABASE_URL
: 연결할 sql db 주소create_engine
: 으로 엔진생성SessionLocal
: ORM specific한 access point 를 위한 class.declarative_base()
로 Base
class (sqlalchemy model) 생성Base
class로부터 상속받아 db 에 해당하는 model class(model) 생성Column
과 ForeignKey
, Integer
등을 활용하여 각 칼럼의 자료형과 key값을 정의해 줌pydantic
의 BaseModel
모듈 이용아래 내용은 The Ultimate Fastapi Tutorial 블로그와 github을 기반으로 작성되었습니다.
파일 구조에 따른 모듈의 역할에 집중해서 이해해 봅시다.
파일 구조
Python class
로부터 table
과 column
을 정의하고자 함*** Declarative Mapping?
declarative_base
)recipe
및 User
db 연결을 위한 class 준비db/base_class.py
- Base
class 생성 : SQLalchemy로 class와 DB를 이어주는 역할!
models/recipe.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class Recipe(Base): # 1
id = Column(Integer, primary_key=True, index=True) # 2
label = Column(String(256), nullable=False)
url = Column(String(256), index=True, nullable=True)
source = Column(String(256), nullable=True)
submitter_id = Column(String(10), ForeignKey("user.id"), nullable=True) # 3
submitter = relationship("User", back_populates="recipes") # 4
1) Base class import 한 것들을 상속받아 Recipe
class 생성
2) sqlalchemy
로 부터 Column
을 import하여 recipe의 id, label, url, source 등을 Column화해주고, String
, Integer
으로 data type을 지정해 줍니다.
3) recipe
와 user
간에 1:多 관계를 정의합니다. 다시말해, 하나의 recipe에 다양한 유저를 mapping합니다.
4) 多:1 관계로도 bidirectional하게 mapping하기 위해 relationship()
을 정의하고 relationship.back_populates
를 이용하여 둘을 연결합니다.
models/user.py
class User(Base):
id = Column(Integer, primary_key=True, index=True)
first_name = Column(String(256), nullable=True)
surname = Column(String(256), nullable=True)
email = Column(String, index=True, nullable=False)
is_superuser = Column(Boolean, default=False)
recipes = relationship(
"Recipe",
cascade="all,delete-orphan",
back_populates="submitter",
uselist=True,
)
db/session.py
에서 Engine에 해당하는 인스턴스 생성from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
SQLALCHEMY_DATABASE_URI = "sqlite:///example.db" # 1
engine = create_engine( # 2
SQLALCHEMY_DATABASE_URI,
# required for sqlite
connect_args={"check_same_thread": False}, # 3
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # 4
```
1) SQLALCHEMY_DATABASE_URI
가 SQLite가 어디서 data를 지속할지 정의합니다.
2) create_engine
함수를 통해 엔진을 생성합니다.
- URI 이외에도 driver
, dialect
, database server location
, users
그리고 passwords and ports 등 훨씬 더 복잡한 string이 들어갈 수 있음.[참고]
3) check_same_thread : False
- Fastapi는 하나의 request안에 다양한 thread의 db에 접근 가능하므로, False로 설정해야 됨.
4) DB Session
만들기 : engine과 달리 ORM-specific하고, database로의 main access point 역할. "holding zone"이라고 표현되어 있음.
Pydantic
model이 validation에 활용되어 CRUD utility
에 전달됨.DB query
를 준비하기 위해 ORM Session
과 shaped data structure
을 함께 이용혼동 금지🤣
name = Column(String)
: SQLAlchemy model
name : str
: Pydantic model
schemas/recipe.py
from pydantic import BaseModel, HttpUrl
from typing import Sequence
class RecipeBase(BaseModel):
label: str
source: str
url: HttpUrl
class RecipeCreate(RecipeBase):
label: str
source: str
url: HttpUrl
submitter_id: int
class RecipeUpdate(RecipeBase):
label: str
# Properties shared by models stored in DB
class RecipeInDBBase(RecipeBase):
id: int
submitter_id: int
class Config:
orm_mode = True
# Properties to return to client
class Recipe(RecipeInDBBase):
pass
# Properties properties stored in DB
class RecipeInDB(RecipeInDBBase):
pass
orm_mode
: dictionary 자료형이 아니라도 읽을 수 있게 해줌orm_mode
가 아니라면, path operation으로부터 SQLAlcemy model을 반환했을 때 relationship data는 반환하지 않을 것임.
Recipe
와RecipeInDB
분리하는 이유?
- DB와 관련된 것만 분리해서 받을 수 있게 만듦-> Pydantic Schema 뿐 아니라 database와 상호작용할 reusable function 필요
- CRUD directory 안에 이러한 data access layer가 정의됨!
insert/update/delete
crud/base.py
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.db.base_class import Base
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): # 1
def __init__(self, model: Type[ModelType]): # 2
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
**Parameters**
* `model`: A SQLAlchemy model class
* `schema`: A Pydantic model (schema) class
"""
self.model = model
def get(self, db: Session, id: Any) -> Optional[ModelType]:
return db.query(self.model).filter(self.model.id == id).first() # 3
def get_multi(
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all() # 4
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data) # type: ignore
db.add(db_obj)
db.commit() # 5
db.refresh(db_obj)
return db_obj
# skipping rest...
CRUDBase
Class 정의pydantic
으로부터 BaseModel 2개 import 해서 총 3개 input으로 넣음(ModelType
, CreateSchemaType
, UpdateSchemnaType
)Modeltype
이 인스턴스 생성 시 input으로 들어감get
method : 하나의 database row를 가져온다.Session
: sqlalchemy로부터 import된 모듈로 db
를 입력받음.query
: 다른 DB query들을 하나로 묶는 방법get_multi
: 여러 database row 가져오기.offset
과 .limit
이용, all()
로 마무리.commit()
commit()
필요from app.crud.base import CRUDBase
from app.models.recipe import Recipe
from app.schemas.recipe import RecipeCreate, RecipeUpdate
class CRUDRecipe(CRUDBase[Recipe, RecipeCreate, RecipeUpdate]): # 1
...
recipe = CRUDRecipe(Recipe) # 2
각 모듈의 출처를 유념하면서 아래를 확인해 봅시다.
ModelType : Recipe
CreateSchemaType : RecipeCreate
UpdateSchemnaType : RecipeUpdate
이후 recipe class 생성
중간정리😎
- 정의된 Base Class로부터
User/Recipe
class 선언. 이는 sqlalchemy로부터 생성되었고 db 자료형등을 정의함.session.py
에서 sqlalchemyengine
및session
정의함schemas/recipe.py
에서 pydantic class 정의함.(이는 request온 정보를 validation하기 위함)- CRUD Utility를 위해
CRUDBase
정의하고,sqlalchemy
class와pydantic
class 활용하여 생성
alembic
library 활용하여 이를 해결 가능함env.py
: database connection, 및 sqlalchemy engine, class 선언과 관련한 환경설정version.py
: migration이 작동하기 위한 directoryscript.py.mako
, README
: alembic으로부터 생성된 boilerplate(표준 문안)prestart.sh
: migration command#! /usr/bin/env bash
# Let the DB start
python ./app/backend_pre_start.py
# Run migrations
alembic upgrade head <---- ALEMBIC MIGRATION COMMAND
# Create initial data in DB
python ./app/initial_data.py
database 변화뿐만 아니라 table/column 을 처음에 만도는 과정을 포함
backend_pre_start.py
: SQL SELECT 1
initial_data.py
: db/init_db.py
로부터 init_db
함수를 사용한다.
db/init_db.py
: DB 생성!from app import crud, schemas
from app.db import base # noqa: F401
from app.recipe_data import RECIPES
logger = logging.getLogger(__name__)
FIRST_SUPERUSER = "admin@recipeapi.com"
def init_db(db: Session) -> None: # 1
if FIRST_SUPERUSER:
user = crud.user.get_by_email(db, email=FIRST_SUPERUSER) # 2
if not user:
user_in = schemas.UserCreate(
full_name="Initial Super User",
email=FIRST_SUPERUSER,
is_superuser=True,
)
user = crud.user.create(db, obj_in=user_in)
else:
logger.warning(
"Skipping creating superuser. User with email "
f"{FIRST_SUPERUSER} already exists. "
)
if not user.recipes:
for recipe in RECIPES:
recipe_in = schemas.RecipeCreate(
label=recipe["label"],
source=recipe["source"],
url=recipe["url"],
submitter_id=user.id,
)
crud.recipe.create(db, obj_in=recipe_in) # 3
init_db
는 Session
object(sqlalchemy로부터온)만을 인자로 받음.(import 참조)submitter
를 inital recipe
에 할당app/recipe_data.py
iterating 및 RecipeCreate
schema 적용이하 pip install poetry, install sqlite 등 과정 생략
-app/main.py
확인 시 db argument를 추가로 받는 것을 확인 가능
from fastapi import Request, Depends
# skipping...
@api_router.get("/", status_code=200)
def root(
request: Request,
db: Session = Depends(deps.get_db), # db 인자, Depends class 사용
) -> dict:
"""
Root GET
"""
recipes = crud.recipe.get_multi(db=db, limit=10)
return TEMPLATES.TemplateResponse(
"index.html",
{"request": request, "recipes": recipes},
)
# skippping...
Depends
함수 임포트하여 사용db: Session = Depends(deps.get_db)
Dependency injection이란? 🤔
- 함수에서 작동해야하는 것들을 선언하는 방법(db를 선언한 것처럼)
deps.py
: dependency 추가한 또다른 모듈from typing import Generator
from app.db.session import SessionLocal # 1
def get_db() -> Generator:
db = SessionLocal() # 2
try:
yield db # 3
finally:
db.close() # 4
app/db/session.py
로부터 SessionLocal
class 임포트db
인스턴스 생성yield
문을 통해 database와 효율적인 connection 유지finally
구문을 통해 DB session을 닫아놓음, @api_router.get("/recipe/{recipe_id}", status_code=200, response_model=Recipe) # 1
def fetch_recipe(
*,
recipe_id: int,
db: Session = Depends(deps.get_db),
) -> Any:
"""
Fetch a single recipe by ID
"""
result = crud.recipe.get(db=db, id=recipe_id) # 2
if not result:
# the exception is raised, not returned - you will get a validation
# error otherwise.
raise HTTPException(
status_code=404, detail=f"Recipe with ID {recipe_id} not found"
)
return result
response_model=Recipe
가 이제는 pydantic model Recipe
를 인자로 받는다. 이는 ORM call과 함께 작동한다는 것을 의미한다.
crud
유틸리티 함수 이용 -> db session 객체를 dependency로서 통과시키면서 recipe_id
로 recipe를 호출!
** CRUD utility 함수로 endpoint의 db query가 조절되었음.