FastAPI を使ってWEBアプリを作ってみる その3
前回の投稿ではPostgreSQLコンテナを立ち上げてAlembicからマイグレートするところまで実践しました。
今回の投稿では用意したテーブルとAPIを接続してクエリを実行し、DBから取得した情報を元にJSONを返却するようにエンドポイント構築をおこないます。
過去の投稿はこちらから辿ることができます。
FastAPI を使ってWEBアプリを作ってみる その1 | FastAPIとDockerでHelloWorld |
FastAPI を使ってWEBアプリを作ってみる その2 | AlembicとPostgreSQLでDB Migrate |
Modelの作成
FastAPIはデータ検証に Pydantic を使用するため依存関係の追加をおこないます。
fastapi==0.63.0
uvicorn==0.13.3
pydantic==1.7.3 # 追加
databases[postgresql]==0.4.1
SQLAlchemy==1.3.22
alembic==1.5.2
psycopg2==2.8.6
尚、FastAPIのドキュメントではPydanticに関してこのように説明しています。
すべてのデータバリデーションは Pydantic によって内部で実行されるため、Pydanticの全てのメリットが得られます。そして、安心して利用することができます。
https://fastapi.tiangolo.com/ja/tutorial/path-params/?h=+pydantic#pydanticstr
、float
、bool
および他の多くの複雑なデータ型を型宣言に使用できます。
続いてすべてのモデルで共有する共通ロジックを用意しましょう、core.py ファイルを作成します。
$ mkdir backend/app/models
$ touch backend/app/models/__init__.py backend/app/models/core.py backend/app/models/hedgehog.py
from pydantic import BaseModel
class CoreModel(BaseModel):
pass
class IDModelMixin(BaseModel):
id: int
Pydantic から提供される BaseModel
はデータの検証とデータ型を強制してくれる機能を有しています。
Untrusted data can be passed to a model, and after parsing and validation pydantic guarantees that the fields of the resultant model instance will conform to the field types defined on the model.
型が定かではないデータもモデルに渡すことができ、解析・検証されることで生成するインスタンスのフィールドは定義された型に適合することをpydanticが保証する。
https://pydantic-docs.helpmanual.io/usage/models/
上記はPydanticの公式ドキュメントでの記述です。
新しいモデルを作成する際はこの CoreModel
クラスから継承するようにします。
現在は何もしない状態ですが、いずれモデル間でロジックを共有できるように拡張していきます。
IDModelMixin
クラスはデータベースから出てくる全てのリソースに使用します。
idにデフォルト値を定義しないことで、このフィールドがすべての新しいインスタンスに必要であることをPydantic に伝えています。
idは intを指定しているので文字列・ バイト・float は int に強制的に変換され、変換できない値だった際は例外が投げられます。
早速これらのコアモデルを継承し、ハリネズミさんの情報を格納するモデルを作ります。
from enum import Enum
from typing import Optional
from app.models.core import CoreModel, IDModelMixin
class ColorType(str, Enum):
solt_and_pepper = "SOLT & PEPPER"
dark_grey = "DARK GREY"
chocolate = "CHOCOLATE"
class HedgehogBase(CoreModel):
name: Optional[str]
description: Optional[str]
age: Optional[float]
color_type: Optional[ColorType]
class HedgehogCreate(HedgehogBase):
name: str
color_type: ColorType
class HedgehogUpdate(HedgehogBase):
description: str
age: float
class HedgehogInDB(IDModelMixin, HedgehogBase):
name: str
age: float
color_type: ColorType
class HedgehogPublic(IDModelMixin, HedgehogBase):
pass
CoreModel
を継承し HedgehogBase
モデルを作成、そしてその HedgehogBase
を継承し HedgehogCreate
とHedgehogUpdate
モデルを作成しました。HedgehogInDB
と HedgehogPublic
モデルでは HedgehogBase
の他に IDModelMixin
を継承しています。
Optional
を使用するとインスタンス作成時に渡されなかった属性には None
が設定されます。
上記の他に ColorType
というクラスを定義しています。Enum
を継承すると有効な値を明示的に制限することができます。
今回のケースでは color_type にセットできる値は ColorType
クラスで定義した3種類のみに制限しています。
今回作成した5つのモデルは、ほぼすべてのリソースで使用されるパターンを示しています。
- Base: 全リソースで共有する属性
- Create: 新しいリソースを作成する際に必須の属性
- Update: 更新することが可能な属性
- InDB: データベースから取得するリソースに存在する属性
- Public: GET, POST, PUTリクエストで返されるデータに存在する属性
リポジトリの作成
リポジトリレイヤーを実装する目的は、DBアクションの上に抽象化レイヤーとして機能させることです。
このリポジトリは特定のリソースに対しデータベース機能をカプセル化し、ロジックをアプリケーションから分離することができます。
より詳細なリポジトリパターンの詳細についてはMicrosoftが出しているドキュメントが珍しく理解しやすので参照してみてください。
それでは早速実装していきます。
まずはベースとなるリポジトリを作成してからハリネズミさん用のリポジトリを用意します。
from databases import Database
class BaseRepository:
def __init__(self, db: Database) -> None:
self.db = db
この BaseRepository
はデータベースコネクションへの参照を保持するだけのシンプルなクラスです。
ゆくゆくは一般的なデータベースアクションを追加しますが、まずはミニマムスタートでいきましょう。
続いてハリネズミさん用のリポジトリを用意します。
$ touch backend/app/db/repositories/hedgehogs.py
from app.db.repositories.base import BaseRepository
from app.models.hedgehog import HedgehogCreate, HedgehogInDB
CREATE_HEDGEHOG_QUERY = """
INSERT INTO hedgehogs (name, description, age, color_type)
VALUES (:name, :description, :age, :color_type)
RETURNING id, name, description, age, color_type;
"""
class HedgehogsRepository(BaseRepository):
async def create_cleaning(self, *, new_hedgehog: HedgehogCreate) -> HedgehogInDB:
query_values = new_hedgehog.dict()
hedgehog = await self.db.fetch_one(query=CREATE_HEDGEHOG_QUERY, values=query_values)
return HedgehogInDB(**hedgehog)
BaseRepository と 先ほど作成したHedgehogリソースに関連するいくつかのモデルをインポートしています。
そして databasesパッケージが期待する :query_arg
スタイルでSQLクエリを定義しました。
リポジトリパターンを使用する利点として、生SQLの柔軟性とORMのクリーンなインターフェイスがどちらも得られることが挙げられます。
HedgehogsRepository はBaseRepositoryを継承し、postgresデータベースに新しいハリネズミさんを登録するための create_hedgehog を定義しています。
create_hedgehogメソッドは、HedgehogCreateモデルを使用して型アノテーションされたnew_hedgehog引数としています。
query_values = new_hedgehog.dict()
hedgehog = await self.db.fetch_one(query=CREATE_HEDGEHOG_QUERY, values=query_values)
そしてこのようにPydanticモデルはdictメソッドで辞書型に変換するとSQLクエリの引数にマップさせることができます。
Pydantic ではこのようにモデルをエクスポートする際に様々な形式に変換することができます。
興味がある型は公式のドキュメントを参照してみてください。
依存性の注入
FastAPIではシンプルで簡単な依存性注入が用意されています。
“Dependency Injection" means, in programming, that there is a way for your code (in this case, your path operation functions) to declare things that it requires to work and use: “dependencies".
“依存性注入 “とは、プログラミングにおいてコードを動作させたり使用するために必要な、依存関係を宣言する方法のことを意味します。
https://fastapi.tiangolo.com/tutorial/dependencies/
これから構築するAPIエンドポイントで言えば、データベースアクセスする必要があるため PostgreSQL が依存関係として挙がります。
それではまずは依存性注入で使用するディレクトリを作成し database.pyファイルを作成します。
$ mkdir backend/app/api/dependencies
$ touch backend/app/api/dependencies/__init__.py backend/app/api/dependencies/database.py
from typing import Callable, Type
from app.db.repositories.base import BaseRepository
from databases import Database
from fastapi import Depends
from starlette.requests import Request
def get_database(request: Request) -> Database:
return request.app.state._db
def get_repository(Repo_type: Type[BaseRepository]) -> Callable:
def get_repo(db: Database = Depends(get_database)) -> Type[BaseRepository]:
return Repo_type(db)
return get_repo
get_repository 関数は Repo_type パラメータを持ち get_repo という別の関数を返します。
get_repo 関数では db パラメータがありget_database関数で返される、FastAPI ステートのdb に依存しています。
続いて最初の投稿で作成した route/hedgehogs.py を更新します。
from typing import List
from app.api.dependencies.database import get_repository
from app.db.repositories.hedgehogs import HedgehogsRepository
from app.models.hedgehog import HedgehogCreate, HedgehogPublic
from fastapi import APIRouter, Body, Depends
from starlette.status import HTTP_201_CREATED
router = APIRouter()
@router.get("/")
async def get_all_hedgehogs() -> List[dict]:
hedgehogs = [
{"id": 1, "name": "momo", "color": "SALT & PEPPER", "age": 2},
{"id": 2, "name": "coco", "color": "DARK GREY", "age": 1.5}
]
return hedgehogs
@router.post("/",
response_model=HedgehogPublic,
name="hedgehogs:create-hedgehog",
status_code=HTTP_201_CREATED)
async def create_new_hedgehog(
new_hedgehog: HedgehogCreate = Body(..., embed=True),
hedgehogs_repo: HedgehogsRepository = Depends(get_repository(HedgehogsRepository)),
) -> HedgehogPublic:
created_hedgehog = await hedgehogs_repo.create_hedgehog(new_hedgehog=new_hedgehog)
return created_hedgehog
create_new_hedgehog 関数のパラメータである、hedgehogs_repo の実態は先のget_repository 関数から返されるデータベース参照を HedgehogRepository に渡しPostgreSQL とインターフェイスで接続させる、という依存性注入となっています。
ここまで作成できたら早速エンドポイントを検証してみましょう!
インタラクティブにAPIコール可能な Swagger API Document を開いて下さい。
緑色に表示されている POST メソッドの部分をクリックするとAPIの詳細が表示されます。
画面右側にある [Try it out] を押下するとリクエストボディの編集画面が出てくるので試しに少し書き換えてみます。
{
"new_hedgehog": {
"name": "momo",
"description": "momo is so cute!",
"age": 2,
"color_type": "SOLT & PEPPER"
}
}
書き換えたら Execute
を押下してください。
実際にAPIエンドポイントにJSONが渡されレコードの差し込みが行われます。
PostgreSQLコンテナに入ってレコードを確認してみましょう。
$ docker-compose exec db psql -h localhost -U postgres --dbname=postgres
postgres=# select * from hedgehogs;
id | name | description | color_type | age
----+------+------------------+---------------+------
1 | momo | momo is so cute! | SOLT & PEPPER | 2.00
(1 row)
momo is so cute!
無事にレコードが作成されていました!
まとめ
今回の投稿ではリポジトリと依存性注入をおこない、PostgreSQLにエンドポイントを接続しました。
これでアプリを構築するために最低限必要だけど少し面倒な作業は終わりです!
次回の投稿では pytest を使用しFastAPIで作ったAPIエンドポイントのテストを実装していきます。
作成したコードはGitHubにアップしています。
その3はpart3ブランチが対応しています。