1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Django Ninja に Dependency Injection を導入する

Posted at

はじめに

django-ninja は Django に対し FastAPI のような使い勝手を提供するためのライブラリで、個人的に非常に気に入っています。

django-ninja プロジェクトに Dependency Injection を導入する方法を調べたので個人的な備忘として残しておきます。

基本

DI ライブラリとして AnyDI を使用します。
※django-ninja 側の Issue で AnyDI という DI ライブラリが紹介されていたのをきっかけに知りました。

Ref: Ninja + dependency injector? · Issue #1406 · vitalik/django-ninja

AnyDI は型安全、非同期サポートなど様々な特徴が謳われています。

FastAPI、Django/django-ninja との統合がサポートされています。
基本的な使い方から紹介します。

参考: https://anydi.readthedocs.io/en/latest/extensions/django/

settings.py
INSTALLED_APPS = [
    ...
    "anydi.ext.django",
]

# ...
ANYDI = {
    "INJECT_URLCONF": "my_proj.urls",
    # or "INJECT_URLCONF": ROOT_URLCONF,
    "PATCH_NINJA": True,
}
my_proj/urls.py
from django.urls import path

from ninja import NinjaAPI

api = NinjaAPI()

# ルーティングの設定
from my_app.routers import router as my_app_router
api.add_router("", my_app_router)

urlpatterns = [
    path("api/", api.urls)
]
# --- my_app/user_service.py
from dataclasses import dataclass

import anydi
from anydi import singleton

@singleton
class UserRepository:
    def create(self) -> None:
        print("Create User  in UserRepository.")
        print("User created in UserRepository.")

@singleton
@dataclass
class UserService:
    repository: UserRepository

    def create_user(self):
        print("Create User  in UserService.")
        self.repository.create()
        print("User created in UserService.")


# --- my_app/routers.py
from ninja import Router

router = Router()

# ※この記事では API パスやメソッドは本題ではないため簡素化しています
@router.get("/user/create")
def create_user(request, user_service: UserService = anydi.auto) -> dict[str, str]:
    user_service.create_user()
    return {"message": "ok"}

実行結果

$ python manage.py runserver

$ curl http://localhost:8000/api/user/create
{"message":"ok"}

# => runserver のコンソール出力
# [11/May/2025 14:53:22] "GET /api/user/create HTTP/1.1" 200 17
# User created by UserService.
# Create by UserRepository.
# User created by UserRepository.
# User created by UserService.

@router.get("/user/create") にディスパッチされた関数 create_user の引数に UserService インスタンスが渡されてきます。
また UserService の依存として UserRepository のインスタンスも連鎖的に解決され、それぞれ依存性が注入されていることが分かります。

create_user 関数の中で UserService クラスのコンストラクタを呼び出す必要がないため、シンプルに user_service.create_user() と呼び出すだけで済んでいます。

Advanced

AnyDI を含め一般的な DI コンテナの実装は "型/クラスをキーにした dict" のような形式で注入するオブジェクトを管理していますが、AnyDI では同じ型で複数のインスタンスを登録することも可能です。

公式ドキュメントの使用例 (Link) は正直あまり実用的なものと思えなかったため、自分なりに有用な使用例を考えてみました。

例えば「ユーザー作成」というシナリオにおいて、実行ユーザーの権限 (管理者 or 一般ユーザー) に応じて処理を変えたい場合を考えます。

基本方針:

  • ユーザー権限種別は Enum UserRole で管理する
  • ユーザー権限に応じて UserService の実装を切り替える
  • IUserService にインターフェースのみ定義し、具体的な処理を具象クラスに実装する
    ※SOLID 原則に従い具体に依存しないようにする
    • => UserServiceByAdministratorUserServiceByGeneralUser
# --- my_app/user_service.py
from dataclasses import dataclass
from enum import Enum, auto
from typing import Protocol, Annotated

from anydi import singleton, Module, provider
from anydi.ext.django import container

class UserRole(Enum):
    ADMINISTRATOR = auto()
    GENERAL = auto()


class IUserService(Protocol):
    def create_user(self) -> None: ...


@dataclass
class UserServiceByAdministrator(IUserService):
    repository: UserRepository

    def create_user(self) -> None:
        print("Create User  by Administrator.")
        self.repository.create()
        print("User created by Administrator.")


@dataclass
class UserServiceByGeneralUser(IUserService):
    repository: UserRepository
    def create_user(self) -> None:
        print("Create User  by General User.")
        self.repository.create()
        print("User created by General User.")


class UserServiceModule(Module):
    @provider(scope="singleton")
    def by_administrator(self, repository: UserRepository) -> Annotated[IUserService, UserRole.ADMINISTRATOR]:
        return UserServiceByAdministrator(repository)

    @provider(scope="singleton")
    def by_general_user(self, repository: UserRepository) -> Annotated[IUserService, UserRole.GENERAL]:
        return UserServiceByGeneralUser(repository)


container.register_module(UserServiceModule)

# Check if registered in container
assert container.is_registered(Annotated[IUserService, UserRole.ADMINISTRATOR])
assert container.is_registered(Annotated[IUserService, UserRole.GENERAL])

# --- my_app/routers.py
from .user_service import IUserService, UserRole

@router.get("/user/create_by_administrator")
def create_user_by_administrator(
    request, user_service: Annotated[IUserService, UserRole.ADMINISTRATOR] = anydi.auto
) -> dict[str, str]:
    user_service.create_user()
    return {"message": "ok"}


@router.get("/user/create_by_general_user")
def create_user_by_general_user(
    request, user_service: Annotated[IUserService, UserRole.GENERAL] = anydi.auto
) -> dict[str, str]:
    user_service.create_user()
    return {"message": "ok"}
実行結果
$ python manage.py runserver

$ curl 'http://localhost:8000/api/user/create_by_administrator'
{"message":"ok"}

# Create User  by Administrator.
# Create User  in UserRepository.
# User created in UserRepository.
# User created by Administrator.
# [11/May/2025 15:41:17] "GET /api/user/create_by_administrator HTTP/1.1" 200 17

$ curl 'http://localhost:8000/api/user/create_by_general_user'
{"message":"ok"}

# Create User  by General User.
# Create User  in UserRepository.
# User created in UserRepository.
# User created by General User.
# [11/May/2025 15:41:42] "GET /api/user/create_by_general_user HTTP/1.1" 200 17

ポイント:

from anydi.ext.django import containercontainerContainer インスタンス(DI コンテナ)で、グローバルに使える (= シングルトン?)。基本的にこれを使っておけばいいように思う。

class UserServiceModule(Module) が一番のポイント。初見だと混乱するかもしれない。
このクラスで @provider デコレーターとともに定義したメソッドの型ヒントと戻り値が DI コンテナに登録される

  • 戻り値の型ヒント: (DI コンテナの) キー
  • メソッドの戻り値: 値

※また、引数の repository: UserRepository は DI コンテナに登録済みのため解決されている

class UserServiceModule(Module):
    @provider(scope="singleton")
    def by_administrator(self, repository: UserRepository) -> Annotated[IUserService, UserRole.ADMINISTRATOR]:
        return UserServiceByAdministrator(repository)

    @provider(scope="singleton")
    def by_general_user(self, repository: UserRepository) -> Annotated[IUserService, UserRole.GENERAL]:
        return UserServiceByGeneralUser(repository)

最後にモジュールを登録する

container.register_module(UserServiceModule)

これで 1つの型 IUserService に対して2つのインスタンスを登録できた。

利用側(= routers.py)では同じ型ヒントを用いれば DI コンテナがインスタンスを解決してくれる。

my_app/routers.py
@router.get("/user/create_by_administrator")
def create_user_by_administrator(
    request, user_service: Annotated[IUserService, UserRole.ADMINISTRATOR] = anydi.auto
) -> dict[str, str]:
    user_service.create_user()
    return {"message": "ok"}

まとめ

Python の DI ライブラリ AnyDI を使って Django / django-ninja に DI を導入する方法を紹介しました。
まだスター数は少ないですが、高機能で使いやすい DI ライブラリだと思います。

特に、ほとんどカスタマイズすることなくデフォルトの振る舞いで django-ninja と統合できて使い勝手が良かったです。

最後に、この記事で動かしたコードは GitHub にあるのでぜひ試してみてください。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?