mizzsugar’s blog

日々感じていることや学んだことを書きます。エンジニアリング以外にも書くかもしれません。

FastAPIでDI(Dependency Injection)したい

最近、FastAPIを使った開発をしたので、そこでやったことを書きます。

この記事は2020年のAdvent Calendarで書く予定の記事でしたが、間に合わず、2020年12月31日に投稿したものです…涙

使用技術

  • Python 3.8
  • FastAPI 0.61
  • gunicorn 20.0.4
  • uvicorn 0.12.2
  • pydantic 1.7.1

実現したかった構成

依存関係を

view -> domain -> repository

という風に依存するようにしたかったです。 ※domain -> repositoryはこの記事では扱いません。別の記事に書こうかと考えています。

viewにdomainの依存性注入を試みました。

依存性注入のメリットについては、本記事では取り扱いません。気になる方は、こちらの記事が参考になりますのでぜひ読んでみてください。

qiita.com

困ったこと

Python製WEBフレームワークPyramidだと、add_request_method があります。 add_request_method を使って下記のような実装を実現できます。こうすることで、view層にdomainを依存させることができます。 この例だと、domainしか依存させていないので、依存させているdomainのみview層で直接呼び出せるようになりました。

domain.py

class Domain:
    def __init__(self):
        pass

    def add(self, a: int, b: int) -> int:
        return a + b

    def sub(self, a: int, b: int) -> int:
        return a - b

wsgi.py

from typing import Final

import pyramid.config
import pyramid.router

import domain


def _init_domain(config: pyramid.config.Configurator) -> None:
    def request_method() -> domain.Domain:
        return domain.Domain()

    config.add_request_method(request_method, 'domain', reify=True)


def main() -> pyramid.router.Router:
    """ This function returns a Pyramid WSGI application.
    """
    with pyramid.config.Configurator(settings=settings) as config:
        config.include('.views.routes')
        _init_domain(config)
        return config.make_wsgi_app()

views/__init__.py

import http
import json
from typing import Any, Dict, Final, cast

import pydantic
import pyramid


class Error(Exception):
    pass


class MissingParameterError(Error):
    pass


class Addiction(pydantic.BaseModel):
    augend: int
    addend: int


class Subtraction(pydantic.BaseModel):
    subtrahend: int
    minuend: int


def _take_json(request: pyramid.request.Request) -> Dict[str, Any]:
    try:
        return cast(Dict[str, Any], request.json)
    except json.JSONDecodeError:  # Request BodyがJSONでない場合に起きます。
        raise MissingParameterError()


def add(
        request: pyramid.request.Request) -> pyramid.response.Response:
    parameter: Final = Addiction.parse_obj(_take_json(request))
    result: Final = request.domain.add(parameter.augend, parameter.addend)
    return pyramid.response.Response(
        status=http.HTTPStatus.OK,
        json={'result': result},
    )


def sub(
        request: pyramid.request.Request) -> pyramid.response.Response:
    parameter: Final = Subtraction.parse_obj(_take_json(request))
    result: Final = request.domain.sub(parameter.subtrahend, parameter.minuend)
    return pyramid.response.Response(
        status=http.HTTPStatus.OK,
        json={'result': result},
    )

views/routes.py

import pyramid.config

import views


def includeme(config: pyramid.config.Configurator) -> None:
    for pattern, view in [
            ('/add', views.add),
            ('/sub', views.sub),
    ]:
        config.add_route(pattern, pattern, request_method='POST')
        config.add_view(view, route_name=pattern, request_method='POST')

起動コマンド

poetry run waitress-serve --call wsgi.main


FastAPIでこれ相当のことをする方法を探すのに苦戦しました。それで、今回の記事を書こうと決めました。


Domainオブジェクト→view層に依存を実現するためのDepends

View層に直接依存するものは、FastAPIの機能の一つ、Dependsを使いました。

Dependencies - First Steps - FastAPI

Dependsは、DIを実現するために、FastAPIが用意してくれている仕組みです。

views.py

from typing import Final

import fastapi
import fastapi.responses
import pydantic

import domain  # 内容は上記のdomain/__init__.pyと同じです。


def _domain_factory() -> domain.Domain:
    return domain.Domain()


class Addiction(pydantic.BaseModel):
    augend: int
    addend: int


def add(
    addiction: Addiction,  # FastAPIでは、request bodyとなるオブジェクトを引数で受け取ります。
    domain: domain.Domain = fastapi.Depends(_domain_factory),  # domain.Domainを返す関数をDependsに渡します。
) -> fastapi.responses.JSONResponse:
    result: Final = domain.add(addiction.augend, addiction.addend)
    return fastapi.responses.JSONResponse({'result': result})

routes.py

import fastapi

import views


def add_routes(app: fastapi.FastAPI) -> None:
    app.add_api_route(
        "/add",
        views.add,
        methods=["POST"],
    )
    app.add_api_route(
        "/sub",
        views.sub,
        methods=["POST"],
    )

wsgi.py

from typing import Final

import fastapi

import routing


def main() -> fastapi.FastAPI:
    app: Final = fastapi.FastAPI()
    routing.add_routes(app)

    return app

起動コマンド

gunicorn 'app.wsgi:main()' -k uvicorn.workers.UvicornWorker


view層の関数の引数として、依存させたいオブジェクトを渡します。 引数の名前は、view層の関数内で利用したい変数名にします。

注意しないといけないのは、Dependsにdomain.Domainを返す関数を渡すことです。(理由は分かりませんが、Domainに引数となる値を渡す場合、関数を渡さないと関数定義時の値が評価されるからでしょうか? 実行時の値が評価されないと、日時オブジェクトなどは意図しない値になりますからね)

FastAPIでもPyramidのように、view -> domainの依存性注入を実現できることが分かりました。

実際のプロジェクトでは domain -> repositoryの依存性注入もしましたが、長くなってしまうので別の記事に分けて書こうかと思います。