FastAPI を使ってWEBアプリを作ってみる その3

2021-01-25Python,TIPSDocker,FastAPI,PostgreSQL

前回の投稿ではPostgreSQLコンテナを立ち上げてAlembicからマイグレートするところまで実践しました。
今回の投稿では用意したテーブルとAPIを接続してクエリを実行し、DBから取得した情報を元にJSONを返却するようにエンドポイント構築をおこないます。

過去の投稿はこちらから辿ることができます。

FastAPI を使ってWEBアプリを作ってみる その1FastAPIとDockerでHelloWorld
FastAPI を使ってWEBアプリを作ってみる その2Alembicと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の全てのメリットが得られます。そして、安心して利用することができます。
str、 float 、 bool および他の多くの複雑なデータ型を型宣言に使用できます。

https://fastapi.tiangolo.com/ja/tutorial/path-params/?h=+pydantic#pydantic


続いてすべてのモデルで共有する共通ロジックを用意しましょう、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 を継承し HedgehogCreateHedgehogUpdate モデルを作成しました。
HedgehogInDBHedgehogPublic モデルでは 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ブランチが対応しています。

2021-01-25Python,TIPSDocker,FastAPI,PostgreSQL

Posted by Kenny