0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPIとSQLAlchemyとPydanticを使ってCRUD用APIを実装しよう!

Last updated at Posted at 2025-02-15

概要

  • FastAPI
  • SQLAlchemy
  • Pydantic

を使ってCRUD操作のAPIを実装します
今回はタスク管理アプリのCRUD操作を例に説明します

前提

  • FastAPI、SQLAlchemy、Pydanticともにインストール済み
  • 今回はFastAPIとPostgres用のコンテナを使用します
  • SQLAlchemyのバージョン2以上を使用
  • Pydanticのバージョン2以上を使用
  • FastAPIのバージョン0.95.0を使用

ディレクトリ構成

・
├── application
│   ├── __init__.py
│   ├── alembic.ini
│   ├── database.py
│   ├── main.py
│   ├── models.py
│   ├── poetry.lock
│   ├── pyproject.toml
│   ├── schemas.py
├── containers
│   ├── fastapi
│   │   └── Dockerfile
│   └── postgres
│       ├── Dockerfile
│       └── init.sql
└── docker-compose.yml
  • database.py
  • models.py
  • schemas.py
  • main.py

の順に実装していきます

実装

データベースの設定

FastAPIのアプリケーションとDBを接続する設定をdatabase.pyに記載します

database.py
import os

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

SQLALCHEMY_DATABASE_URL = os.environ.get("SQLALCHEMY_DATABASE_URL")

# https://docs.sqlalchemy.org/en/20/core/engines.html#engine-configuration
engine = create_engine(SQLALCHEMY_DATABASE_URL)

# https://docs.sqlalchemy.org/en/20/orm/session_basics.html#session-basics
# DBセッションのオブジェクトを生成
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# https://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.DeclarativeBase
class Base(DeclarativeBase):
    pass

順番に解説します
まず、SQLALCHEMY_DATABASE_URLという変数を定義します
変数内にFastAPIとDBが接続できるためのURLを設定し、それを元にDB用エンジンのオブジェクトを生成します

SQLALCHEMY_DATABASE_URL = os.environ.get("SQLALCHEMY_DATABASE_URL")

engine = create_engine(SQLALCHEMY_DATABASE_URL)

URLのフォーマットは以下ドキュメントを参考にしてください

urlは以下のようになります

SQLALCHEMY_DATABASE_URL=postgresql://{ユーザ名}:{パスワード}@{ホスト名}:5432/{DB名}

その後、DBセッションのオブジェクトを定義します
APIを記載するmain.pyで後ほど使用します

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

Baseクラスを元にモデルを定義します
SQLAlchemyのバージョン2以降からdeclarative_baseではなく、DeclarativeBaseクラスを使うことが推奨されています
modelを定義するmodels.pyで使用します

class Base(DeclarativeBase):
    pass

modelの実装

Todosというmodelを実装します
database.pyで定義したBaseクラスを継承することで作成したmodelの定義をDBに反映させることができます
カラムはsqlalchemyのColumnなどを元に定義していきます
カラムの詳細は以下を参考にしてください

models.py
from database import Base
from sqlalchemy import Boolean, Column, Integer, String


class Todos(Base):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String)
    description = Column(String)
    priority = Column(Integer)
    complete = Column(Boolean, default=False)

schemaの実装

schemas.pyバリデーションを定義します
TodoModelはAPIでPostやPutする際のリクエストのバリデーションや型チェックに使用します
TodoResponseはAPIのレスポンスで出力したいフィールドを定義する際に使用します

schemas.py
from pydantic import BaseModel, Field


class TodoModel(BaseModel):
    title: str = Field(min_length=3)
    description: str = Field(min_length=3, max_length=100)
    priority: int = Field(gt=0, lt=6)
    complete: bool


class TodoResponse(BaseModel):
    id: int = Field()
    title: str = Field(min_length=3)
    description: str = Field(min_length=3, max_length=100)
    priority: int = Field(gt=0, lt=6)
    complete: bool

APIの実装

DBの接続設定、modelの定義、バリデーションの設定が完了したのでAPIの実装に入ります

main.py
from typing import Annotated, List

from database import SessionLocal, engine
from fastapi import Depends, FastAPI, HTTPException, status
from models import Base, Todos
from schemas import TodoModel, TodoResponse
from sqlalchemy import select, update
from sqlalchemy.orm import Session

app = FastAPI()

# https://docs.sqlalchemy.org/en/20/core/metadata.html#sqlalchemy.schema.MetaData.create_all
# テーブルを作成
Base.metadata.create_all(bind=engine)


# https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/?h=get_db
async def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-session-dependency
# 依存性注入をすることでget_dbメソッドが自動的に呼ばるので毎回セッションインスタンスの生成やセッションを切る処理を書かずに済む
# Annotatedを使うことでDepends(get_db)がSession型だとわかる
db_dependency = Annotated[Session, Depends(get_db)]

# https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/?h=get_db#sub-dependencies-with-yield
# https://github.com/fastapi/fastapi/pull/9298
# FastAPI0.95.0以降の機能
@app.get("/api/todos", response_model=List[TodoResponse])
def read_todos(db: db_dependency):
    # https://docs.sqlalchemy.org/en/20/orm/quickstart.html#simple-select
    # https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.scalars
    # scalarsを使うことでTodosのインスタンスを返す
    todos = db.scalars(select(Todos).order_by(Todos.id)).all()
    return todos


@app.get("/api/todos/{todo_id}", response_model=TodoResponse)
def read_todo(db: db_dependency, todo_id: int):
    todo = db.get(Todos, todo_id)
    if not todo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found"
        )
    return todo


@app.post(
    "/api/todos", response_model=TodoResponse, status_code=status.HTTP_201_CREATED
)
def create_todo(db: db_dependency, todo_model: TodoModel):
    # pydantic2ではdict()ではなく、model_dumpが使用されている
    # https://docs.pydantic.dev/latest/concepts/serialization/#modelmodel_dump
    todo = Todos(**todo_model.model_dump())
    # https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.add
    db.add(todo)
    db.commit()
    # # refreshをすることでauto-incrementしたIDやcreated_atが反映されるようになる
    # # https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.refresh
    db.refresh(todo)
    return todo


@app.put("/todo/{todo_id}", response_model=TodoResponse)
async def update_todo(db: db_dependency, todo_model: TodoModel, todo_id: int):
    todo = db.get(Todos, todo_id)
    if not todo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found"
        )
    # https://docs.sqlalchemy.org/en/20/core/dml.html#sqlalchemy.sql.expression.update
    db.execute(
        update(Todos).where(Todos.id == todo_id).values(**todo_model.model_dump())
    )
    db.commit()
    return todo


@app.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(db: db_dependency, todo_id: int):
    todo = db.get(Todos, todo_id)
    if not todo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found"
        )
    # https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.delete
    db.delete(todo)
    db.commit()

順番に解説します
以下の記述を記載することでdatabase.pyで定義したengineの情報を元にDB内にテーブルを作成します

Base.metadata.create_all(bind=engine)

get_dbメソッドを使ってDBのセッションを作成し、使用後に接続を閉じるよう定義します
レスポンスを作成する前に実行されるのは以下の箇所です

    db = DBSession()
    try:
        yield db

以下の箇所はレスポンスが作成される前とレスポンスを返す前に実行されます

    finally:
        db.close()

詳細は以下の通りです

async def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

下記の記述で依存性を注入しています
依存性注入をするとAPI内でdb_dependencyを定義する際にget_dbメソッドが自動的に呼ばるため、毎回セッションインスタンスの生成(db = SessionLocal())やセッションを切る処理(db.close())を書かずに済みます
また、Annotatedを使うことでDepends(get_db)がSession型だとわかるので便利です

db_dependency = Annotated[Session, Depends(get_db)]

まず、一覧表示のAPIを実装します
先ほど定義したdb_dependencyを使って依存性の注入を実現させます
scalarsを使ってTodosのインスタンスをレスポンスとして返します

@app.get("/api/todos", response_model=List[TodoResponse])
def read_todos(db: db_dependency):
    todos = db.scalars(select(Todos).order_by(Todos.id)).all()
    return todos

詳細表示のAPIを実装します
idからTodoを取得し、あればレスポンスとして返し、なければ404を返します

@app.get("/api/todos/{todo_id}", response_model=TodoResponse)
def read_todo(db: db_dependency, todo_id: int):
    todo = db.get(Todos, todo_id)
    if not todo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found"
        )
    return todo

作成用のAPIを実装します

pydanticのバージョン2以上ではdict()ではなく、model_dump()を使用します
リクエスト内のフィールドを**でdict型に変換してTodosに代入してTodosのインスタンスを生成します
Todoのインスタンスをセッションにadd()で加えた後にcommit()でTodoの内容をDBにinsertします

    db.add(todo)
    db.commit()

refreshをすることでauto-incrementしたIDやcreated_atが反映されるようになります

@app.post(
    "/api/todos", response_model=TodoResponse, status_code=status.HTTP_201_CREATED
)
def create_todo(db: db_dependency, todo_model: TodoModel):
    todo = Todos(**todo_model.model_dump())
    db.add(todo)
    db.commit()

    db.refresh(todo)
    return todo

更新用のAPIを作成します
該当するidのTodoがなければ404を返します
存在する場合はmodel_dump()で全てのfieldに更新をかけてtodoのインスタンスを返します

@app.put("/todo/{todo_id}", response_model=TodoResponse)
async def update_todo(db: db_dependency, todo_model: TodoModel, todo_id: int):
    todo = db.get(Todos, todo_id)
    if not todo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found"
        )
    # https://docs.sqlalchemy.org/en/20/core/dml.html#sqlalchemy.sql.expression.update
    db.execute(
        update(Todos).where(Todos.id == todo_id).values(**todo_model.model_dump())
    )
    db.commit()
    return todo

削除用APIを作成します
該当するidのTodoがなければ404を返します
存在する場合はセッション内から削除し、commit()でTodoの内容をDBからdeleteしまs

@app.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(db: db_dependency, todo_id: int):
    todo = db.get(Todos, todo_id)
    if not todo:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found"
        )
    # https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.delete
    db.delete(todo)
    db.commit()

実際に実行してみよう!

127.0.0.1:8000/docsへアクセスし、下記のようにAPIの一覧が作成されたら成功です

スクリーンショット 2025-02-11 8.45.27.png

Todoを作成してみます
以下のように/api/todosへPOSTし、Todoが作成されたら成功です
スクリーンショット 2025-02-11 8.46.31.png

スクリーンショット 2025-02-11 8.46.46.png

続いてTodoの一覧と詳細を取得します
以下のように一覧と詳細を取得できれば成功です
スクリーンショット 2025-02-11 8.47.43.png

スクリーンショット 2025-02-11 8.48.36.png

Todoを更新します
/api/todosへPUTし、Todoが更新されたら成功です
スクリーンショット 2025-02-11 8.50.45.png

スクリーンショット 2025-02-11 8.51.01.png

スクリーンショット 2025-02-11 8.51.17.png

最後にTodoを削除します
以下のようにTodoが削除されたら成功です
スクリーンショット 2025-02-11 8.52.02.png

スクリーンショット 2025-02-11 8.52.25.png

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?