Django上級 #1 Async views と ASGI

読了 7分

Django 基礎 7 編 + 中級 7 編を終えたなら、いよいよ 上級 に入ります。上級 7 編は大規模 / 長期運用の Django プロジェクトで出会うテーマ — 非同期、コマンド、クエリ最適化、キャッシング、シグナルの深さ、WebSocket、デプロイのセキュリティ — を扱います。

基礎 #4 FBV中級 #1 CBV の view の上に async def が加わります。Django は 4.1 から ORM まで本格的に非同期をサポートし始め、5.x で定着しました。

WSGI vs ASGI #

Django は長い間 WSGI (Web Server Gateway Interface) の上で生きてきました。同期関数の 1 回の呼び出しが 1 リクエストを処理するモデルです。並行性はワーカープロセス / スレッドを増やして稼ぎます。

ASGI (Asynchronous Server Gateway Interface) はその後継標準。非同期関数WebSocket / HTTP/2 のような長寿命な接続を扱えます。

WSGIASGI
関数の形def app(environ, start_response)async def app(scope, receive, send)
非同期不可ネイティブ
プロトコルHTTP のみHTTP、WebSocket、HTTP/2
サーバーgunicorn、uWSGIuvicorndaphne、hypercorn
並行性モデルプロセス / スレッドイベントループ + コルーチン

Django は両方サポートしています。wsgi.pyasgi.py という 2 つのエントリポイントが一緒にあります。

myproject/asgi.py
import os
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_asgi_application()

ASGI エントリポイントを使えば 同期 view も非同期 view も一緒に 動作します。つまり移行は段階的に可能です。

最初の async view #

myapp/views.py
import asyncio
from django.http import JsonResponse

async def hello(request):
    await asyncio.sleep(0.1)
    return JsonResponse({"hello": "async"})

defasync def に変わっただけです。ルーティングは同じ。

myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("hello/", views.hello),
]

Django は view を呼び出すときに inspect.iscoroutinefunction で自動検出して、非同期ならイベントループで実行します。

いつ価値があるか #

非同期はタダではありません。 単純な ORM 1 回 + レスポンス 1 回なら同期 view の方がシンプルで速いです。非同期に価値がある場面:

  • 外部 HTTP API を複数 同時に 呼び出す
  • WebSocket のような長寿命な接続 (#6)
  • 外部 IO が view の大半を占める
  • 同じプロセスで 多数の同時接続 を扱わなければならないとき

純粋な CPU 作業や単純な CRUD は非同期に移しても得が少ないです。用途に合わせて 選んでください。

ORM の非同期 — a シリーズ #

Django ORM のすべてのクエリメソッドは a プレフィックスの非同期版 を持っています。

同期非同期
Post.objects.get(id=1)await Post.objects.aget(id=1)
post.save()await post.asave()
post.delete()await post.adelete()
Post.objects.create(...)await Post.objects.acreate(...)
Post.objects.filter(...).update(...)await Post.objects.filter(...).aupdate(...)
for p in Post.objects.all()async for p in Post.objects.all()
list(qs)[p async for p in qs]
非同期 view の中で ORM
from django.http import JsonResponse
from .models import Post

async def post_list(request):
    posts = [
        {"id": p.id, "title": p.title}
        async for p in Post.objects.filter(published=True)[:20]
    ]
    return JsonResponse({"posts": posts})

async def post_detail(request, pk):
    post = await Post.objects.aget(pk=pk)
    return JsonResponse({"id": post.id, "title": post.title})

async for と async comprehension が中心パターンです。

⚠ 関係アクセスは依然として同期 #

🚫 非同期 view の中で関係 lazy
async def post_detail(request, pk):
    post = await Post.objects.aget(pk=pk)
    author_name = post.author.name   # SynchronousOnlyOperation

ORM オブジェクトの 外部キー lazy load は同期コード です。非同期コンテキストでそのまま呼ぶと SynchronousOnlyOperation 例外が出ます。

解決:

✅ select_related で先に
async def post_detail(request, pk):
    post = await Post.objects.select_related("author").aget(pk=pk)
    return JsonResponse({"id": post.id, "author": post.author.name})

事前に JOIN で持ってくれば lazy load が起こりません。#3select_related / prefetch_related は非同期ではほぼ必須です。

外部 API の同時呼び出し — async の真価 #

非同期 view が輝く場面。外部サービス 3 つを 同時に 呼ぶページを見ます。

インストール
pip install httpx
同時呼び出し
import asyncio
import httpx
from django.http import JsonResponse

async def dashboard(request):
    async with httpx.AsyncClient(timeout=5.0) as client:
        weather, news, stocks = await asyncio.gather(
            client.get("https://api.weather.example/now"),
            client.get("https://api.news.example/top"),
            client.get("https://api.stocks.example/quote/AAPL"),
        )

    return JsonResponse({
        "weather": weather.json(),
        "news": news.json(),
        "stocks": stocks.json(),
    })

3 つの呼び出しが 逐次の合算ではなく一番遅い 1 つの時間 で終わります。同期 view + requests で同じことをすると 3 つの時間を足した分だけかかります。

sync_to_async / async_to_sync #

Django 陣営の非同期アダプタ。asgiref.sync から来ます。

sync_to_async — 同期関数を非同期から呼ぶ #

非同期 view から同期コードを呼ぶ
from asgiref.sync import sync_to_async
from .legacy import compute_report   # 同期関数

async def report(request):
    data = await sync_to_async(compute_report)(user_id=request.user.id)
    return JsonResponse(data)

デフォルトは 別スレッド で実行されてイベントループを塞ぎません。ORM の a メソッドも内部的にこれを使っています。

thread_sensitive オプション
@sync_to_async(thread_sensitive=True)
def db_work():
    ...

thread_sensitive=True常に同じスレッド で実行 — 同期 ORM のトランザクションのようなスレッドローカルな状態を持つコードに必要です。デフォルト値は True。

async_to_sync — 非同期関数を同期から呼ぶ #

同期 view から非同期コードを呼ぶ
from asgiref.sync import async_to_sync

def my_sync_view(request):
    result = async_to_sync(fetch_data_async)(request.GET["q"])
    return JsonResponse(result)

毎回新しいイベントループを作って閉じるので 頻繁に呼ぶと高くつきます。 本当に必要な場面だけに。

Async middleware #

ミドルウェアも非同期版を作れます。

async middleware
class TimingMiddleware:
    async_capable = True
    sync_capable = False

    def __init__(self, get_response):
        self.get_response = get_response

    async def __call__(self, request):
        import time
        start = time.perf_counter()
        response = await self.get_response(request)
        elapsed = (time.perf_counter() - start) * 1000
        response["X-Elapsed-Ms"] = f"{elapsed:.1f}"
        return response

フラグ 2 つ:

  • async_capable = True — 非同期コンテキストで動作可能
  • sync_capable = False — 同期はサポートしない

両方 True のミドルウェアは Django が自動で適切なモードで呼び出します。ただし同期 / 非同期の境界ができるとアダプタが入り込んでコストが発生します。可能なら 1 モードに統一を。

ASGI サーバー #

非同期 view を立ち上げるには ASGI サーバーが必要です。3 つが標準です。

uvicorndaphnehypercorn
メンテナencodedjangopgjones
HTTP/2サポート非対応サポート
WebSocketサポートChannels の標準サポート
速度最速普通普通
推奨場面一般的な ASGIChannels (#6)HTTP/2 が必要なとき

uvicorn #

インストール / 実行
pip install "uvicorn[standard]"

uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4

プロダクションは gunicorn + uvicorn worker の組み合わせが定番です。

gunicorn + uvicorn worker
pip install gunicorn

gunicorn myproject.asgi:application \
  -k uvicorn.workers.UvicornWorker \
  --workers 4 --bind 0.0.0.0:8000

gunicorn がプロセス管理 (再起動、シグナル、ロギング) を担当し、uvicorn が実際の ASGI 処理を行います。

daphne #

daphne
pip install daphne

daphne -b 0.0.0.0 -p 8000 myproject.asgi:application

Channels (#6) の標準サーバー。

同期 view と非同期 view の共存 #

同じプロジェクトに 2 つが混ざっていても大丈夫です。Django が自動検出。

urls.py — 混ぜて使う
urlpatterns = [
    path("classic/", views.classic_view),     # def
    path("modern/", views.modern_view),        # async def
]

ASGI サーバーで立ち上げれば両方とも正常動作。同期 view は スレッドプールで実行 されます (イベントループを塞がないように)。

WSGI サーバー (gunicorn 一般 worker、uWSGI) で立ち上げると非同期 view は毎リクエストごとに async_to_sync で実行されるので損が大きいです。非同期 view があるなら ASGI サーバー で。

FastAPI との比較 #

モダン Python 実践 シリーズの FastAPI とよく比較されます。

Django (async)FastAPI
出発点フルスタック (admin、ORM、認証すべて付属)マイクロ
非同期部分 / 混合 (ORM の一部は同期)最初から非同期
型ヒント補助的コア動作
OpenAPIDRF + 別途ビルトイン
学習曲線なだらか
場面フルサイト、admin 活用純粋な API

Django を最初から非同期で書く新規プロジェクトは多くありません。普通は 既存の同期 Django に一部 view だけを非同期で 導入します (外部 API、WebSocket)。最初から 100% 非同期 API なら FastAPI のほうが向いています。

よく出会う落とし穴 #

1) SynchronousOnlyOperation #

非同期 view の中で同期 ORM 呼び出しをすると出ます。a メソッド または sync_to_async で包むべきです。

🚫
async def my_view(request):
    post = Post.objects.get(pk=1)   # 同期呼び出し
async def my_view(request):
    post = await Post.objects.aget(pk=1)

2) Lazy な関係アクセス #

上で見たそれ。select_related/prefetch_related で先に。

3) ORM のトランザクションはまだ同期 #

@transaction.atomic デコレータは 同期関数にだけ 使えます。非同期 view でトランザクションが必要なら、同期関数で包んで sync_to_async で呼ぶパターンを使います。

非同期 view のトランザクション
from asgiref.sync import sync_to_async
from django.db import transaction

@sync_to_async
def create_with_tx(data):
    with transaction.atomic():
        post = Post.objects.create(**data)
        Tag.objects.create(post=post, name=data["tag"])
        return post

async def my_view(request):
    post = await create_with_tx({"title": "...", "tag": "..."})

Django 6.x で非同期トランザクションが正式にサポートされる予定です。それまではこのパターンが推奨。

4) ミドルウェア境界のアダプタコスト #

リクエスト処理中に同期↔非同期の境界が何度も生じると、その度にアダプタ (sync_to_async/async_to_sync) が入って累積します。ミドルウェアチェーンは可能なら 1 モードで

まとめ #

今回つかんだもの:

  • WSGI は同期、ASGI は非同期 + WebSocket/HTTP2
  • async def view(request) — 4.1 から本格化
  • ORM の a メソッド — agetacreateaupdateasync for
  • 関係アクセスは依然として同期 — select_related で先に
  • 外部 API の同時呼び出し (asyncio.gather) が非同期の真価
  • sync_to_asyncasync_to_sync で境界をまたぐ
  • async middleware — async_capable/sync_capable フラグ
  • ASGI サーバー — uvicorn (一般)、daphne (Channels)、hypercorn (HTTP/2)
  • 同期 / 非同期 view は共存可能、ASGI サーバーで立ち上げる
  • 非同期は外部 IO の多い場面に価値、単純な CRUD は同期の方がよい
  • @transaction.atomic はまだ同期 — sync_to_async で包む

次回 (#2 Custom management commands) では、運用のもう 1 つの軸 — コマンドラインの作業 を扱います。期限切れトークンの掃除、一括統計の計算、データマイグレーションなど cron で回す作業を Django らしく作る方法。

X