はじめに
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/
INSTALLED_APPS = [
...
"anydi.ext.django",
]
# ...
ANYDI = {
"INJECT_URLCONF": "my_proj.urls",
# or "INJECT_URLCONF": ROOT_URLCONF,
"PATCH_NINJA": True,
}
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 原則に従い具体に依存しないようにする- =>
UserServiceByAdministrator
とUserServiceByGeneralUser
- =>
# --- 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 container
の container
は Container
インスタンス(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 コンテナがインスタンスを解決してくれる。
@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 にあるのでぜひ試してみてください。