MENU

Django Ninjaチュートリアル19(非同期関数のサポート)

Django Ninjaチュートリアル19(非同期のサポート)

前回までの記事はこちらです。

前回までの記事が読まれている前提で書いていきますので、まだ読んでいない方はこちらから読み進めて追いついてください。

それでは始めていきましょう。

目次

非同期関数のサポート

バージョン3.1以降のDjangoでは、async viewサポートを導入しました。
これにより、ネットワークやIOにバインドした効率的な同時ビューを実行することができるようになりました。

これから実行するコードは非同期処理に関するコードです。
Djangoのバージョンが3.1以上であることを確認して進めてください。
Django が3.1よりも前のバージョンである場合は、下記により再度インストールを行ってください。

docker-compose exec backend poetry add Django>=3.1 django-ninja
# 他のターミナルまたはコマンドプロンプトでdocker-compose upを実行した状態で行ってください。(poetryを導入しているため、依存関係は問題ないと思いますが)

async viewのサポートは、以下の場合などにおいて効率的に機能してくれます。

  • ネットワークを介してAPIを呼び出すとき
  • データベースの実行中や待機中にバックグラウンドで別の処理を実行するとき
  • ディスクドライブへの書き込みやディスクドライブからの書き出しをおこなっているとき

Django Ninjaでは、このDjangoのasync viewの機能を最大限に発揮し、かつ操作を簡単に行うことができます。

Django Ninjaにおけるasync viewの活用例

例を見てみます。

import time
from ninja import NinjaAPI

app_v8 = NinjaAPI(
  title = "Django-Ninja Sample App8",
  version = "1.0.8",
)

@app_v8.get("/say-after")
def say_after(request, delay: int, word: str):
  time.sleep(delay)
  return {"saying": word}

この例では、sleep関数を使用し、処理を一時停止し、その後に単語を返すAPIを設定しています。

このAPIを非同期関数として設定するには、asyncキーワードを定義する関数に追加し、対応する処理にawaitを設定するのみです(このとき、time.sleep()asyncio.sleep()に切り替えます)。

import time
import asyncio
from ninja import NinjaAPI

app_v8 = NinjaAPI(
  title = "Django-Ninja Sample App8",
  version = "1.0.8",
)

@app_v8.get("/say-after")
async def say_after(request, delay: int, word: str):
  await asyncio.sleep(delay)
  return {"saying": word}

この関数を設定したら、サーバーを起動し、以下のURLにアクセスします。

http://127.0.0.1:8000/api_v8/say-after?delay=3&word=Nicemetoyou

すると、クエリパラメータとして、delayに渡した秒数だけ停止した後、設定したJSON(wordもクエリパラメータであり、文字列が渡されている)がリターンされます。

また、http://127.0.0.1:8000/api_v8/docs/にアクセスし、APIを試してみます。
以下のような処理になると思います。

delayに設定した秒数だけ「LOADING」となる
その後、時間経過後、実行結果が返される

ちなみに自動生成ドキュメント(上記の画像の例)をクエリパラメータで渡すと、URLは以下のようになります。
http://127.0.0.1:8000/api_v8/say-after?delay=5&word=Nice%20to%20meet%20you%21

このように非同期処理の関数を設定することができます。

同期関数と非同期関数の併用

Django Ninjaでは、同期関数と非同期関数の併用が可能です。
また、APIをこれまでどおり記述するだけで、自動的にルーティングまでしてくれます。
例を見てみます。

import time
import asyncio
from ninja import NinjaAPI

app_v8 = NinjaAPI(
  title = "Django-Ninja Sample App8",
  version = "1.0.8",
)

# 通常の同期関数
@app_v8.get("say-sync")
def say_after_sync(request, delay: int, word: str):
  time.sleep(delay)
  return { "saying": word}

# 非同期関数
@app_v8.get("/say-async")
async def say_after_async(request, delay: int, word: str):
  await asyncio.sleep(delay)
  return {"saying": word}
自動生成ドキュメントでは同期関数も非同期関数も自動生成される

このようにこれまで作成した同期関数も非同期関数も互いに併用することが可能です。

Elasticsearchを使用する

非同期関数のサポートが追加されたElasticsearchを使用する例を見てみます。

Elasticsearchとは?
複数のファイルから特定の文字列を分散検索するソフトウェアです。
「分散検索」するため、同じ処理を順番に実行するのではなく、同時に、かつ異なる文字列を渡して実行するなどの処理ができます。

Elasticsearchのインストール

実際のコードを見ていく前に、Elasticsearchをインストールします。
以下のコードを、他のターミナルでdocker-compose upを実行した状態で、実行してください。

docker-compose exec backend poetry add elasticsearch

実行後、以下のコマンドでdockerを再度ビルドします。

docker-compose build --no-cache

実際のコード

Elasticsearchクラスの代わりに、AsyncElasticsearchクラスを使用し、結果の表示はawaitで実行します。

Elasticseachを使用する場合、hostscloud_idといった引数に値を渡すことが必要ですが、今回は例を示すところまでとします。

import time
import asyncio
from ninja import NinjaAPI
from elasticsearch import AsyncElasticsearch

app_v8 = NinjaAPI(
  title = "Django-Ninja Sample App8",
  version = "1.0.8",
)

es = AsyncElasticsearch()

~略~

@app_v8.get("/search")
async def search(request, q: str):
  resp = await es.search(
    index = "documents",
    body = { "query": {
      "query_string": {
        "query": q
      }
    }},
    size = 20
  )
  return resp["hits"]

elasticsearchインスタンスを作成(es)し、クエリパラメータで渡された値(=q)を検索します。
検索された値は、20件(sizeで指定)を上限に取得されます。

ORMの使用

Djangoの重要な部分では、コルーチンを認識しないため、非同期関数が正常に同期しない場合があります。
このような部分は、async-unsafeとして非同期関数の対象他ならないよう、非同期環境から独立しています。

例えば、以下のような例を作成する場合、エラーが出てしまいます。

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
  blog = Blog.objects.get(pk=post_id)
  ...

そこで、非同期処理のORMが出てくるまでは、sync_to_async()というアダプターを使用します(下記の例を参照)。

from asgiref.sync import sync_to_async

@sync_to_async
def get_blog(post_id):
  return Blog.objects.get(pk = post_id)

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
  blog = await get_blog(post_id)
  ...

sync_to_async()は、非同期処理を設定するときに、非同期ではない関数を使用するためのアダプター関数です。
つまり、非同期関数との連携ができるようにするための関数としてアダプトさせているものです。

上記の例でも、同期関数であるget_blog関数にsync_to_async()を適用することで、その後に記載した非同期関数のsearch関数でget_blogが使用できるようになっています。

もしくは、アダプター関数ではなく、以下のようにsync_to_async関数を直接適用する例も示されています。

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
    blog = await sync_to_async(Blog.objects.get)(pk=post_id)
    ...

なお、Django4.1以降のバージョンでは、非同期ORMが含まれています。
これにより、関数を定義し、その関数を非同期処理の内部で使おうとした場合、定義した関数名の先頭にaを付けて使用することができます。
先ほどの例を書き換えると以下のようになります。

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
    blog = await Blog.objects.aget(pk=post_id)
    ...

最後に

今回は、Django Ninjaにおける非同期処理の設定の仕方を見てきました。
Django4.1以降では非同期サポートが増えてきているものの、まだまだFastAPIのような非同期関数の実行には向いていないような印象でした。
非同期関数による処理を実行することはできますが、同期関数との橋渡しをしないといけないため、設定が冗長化してしまい、コードが見づらくなるかもしれません。
Djangoのmodel昨日からSchemaを生成できる機能などは便利で面白い機能だったが故に、非同期関数のサポートに関しては少し残念でした。
※将来的には改善されるでしょうが、現状ではFastAPIが優勢でしょうか。

公式ドキュメントをベースに学習を進めてきましたが、今回でDjango Ninjaの公式ドキュメントをひととおり見てきたことになります。

わからないところもありましたが、自分では結構勉強になったように感じました。

今後、これまでの記事を見た方の学習の参考になれば嬉しいです。
それでは、ここで一区切りです。
ありがとうございました。

個人的には、事前にFastAPIに触れていたこともありますが、やはり用途によってどのように使い分けるかが重要だと、改めて感じました。
REST APIの作成だけでよければ、DRFかDjango Ninjaは有効だと思います。
型定義によるバリデーションを適用したいのであれば、Django Ninjaとなるでしょう。
しかし、Pythonで非同期関数を使用し、型定義を利用したバリデーションを使用したAPIを作成したいということであればFastAPI一択となるのではないかと思います。
あくまで個人的な(独学者の)意見ですが、そのように感じましたので、ざっくりと書かせていただきました。
参考になれば幸いです。

何かDjango Ninjaを使用したアプリケーションを作りたいと思いましたが、作るものが通常のDjangoやDRFでも作れる範囲のものになってしまう可能性が高く、他のアプリケーションの例とあまり変わり映えしないように感じたため、アプリケーション作成の例は記事にしないこととします。
ご了承ください。

Django Ninjaチュートリアル19(非同期のサポート)

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

はじめまして、ふじです。
Python、Django、FastAPI、React.js、Next.jsを学習している、ずっと文系のプログラミング独学者です。

目次