Django Ninja に Dependency Injection を導入する

# はじめに

[django-ninja](https://github.com/vitalik/django-ninja) は Django に対し FastAPI のような使い勝手を提供するためのライブラリで、個人的に非常に気に入っています。

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

# 基本

DI ライブラリとして [AnyDI](https://github.com/antonrh/anydi) を使用します。  
※django-ninja 側の Issue で AnyDI という DI ライブラリが紹介されていたのをきっかけに知りました。

Ref: [Ninja + dependency injector? · Issue #1406 · vitalik/django-ninja](https://github.com/vitalik/django-ninja/issues/1406)

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

https://github.com/antonrh/anydi?tab=readme-ov-file#anydi

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

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

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

# ...
ANYDI = {
    "INJECT_URLCONF": "my_proj.urls",
    # or "INJECT_URLCONF": ROOT_URLCONF,
    "PATCH_NINJA": True,
}
```

```python: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)
]
```

```python
# --- 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"}
```

実行結果

```bash
$ 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](https://anydi.readthedocs.io/en/latest/usage/#annotated-providers)) は正直あまり実用的なものと思えなかったため、自分なりに有用な使用例を考えてみました。  

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

基本方針:

- ユーザー権限種別は Enum `UserRole` で管理する
- ユーザー権限に応じて `UserService` の実装を切り替える
- `IUserService` にインターフェースのみ定義し、具体的な処理を具象クラスに実装する
※SOLID 原則に従い具体に依存しないようにする
  - => `UserServiceByAdministrator` と `UserServiceByGeneralUser`


```python
# --- 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"}
```

<details>
<summary>実行結果</summary>

```bash
$ 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
```

</details>


### ポイント:

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

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

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

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

```python
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)
```

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

```python
container.register_module(UserServiceModule)
```

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

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

```python: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 にあるのでぜひ試してみてください。

https://github.com/skokado/django-ninja-di