Django上級 #1 Async views と ASGI
Django 基礎 7 編 + 中級 7 編を終えたなら、いよいよ 上級 に入ります。上級 7 編は大規模 / 長期運用の Django プロジェクトで出会うテーマ — 非同期、コマンド、クエリ最適化、キャッシング、シグナルの深さ、WebSocket、デプロイのセキュリティ — を扱います。
- #1 Async views と ASGI ← 今回
- #2 Custom management commands
- #3 クエリ最適化
- #4 キャッシング
- #5 Signals の深さとトランザクション後処理
- #6 Django Channels — WebSocket
- #7 デプロイのセキュリティ
基礎 #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 のような長寿命な接続を扱えます。
| WSGI | ASGI | |
|---|---|---|
| 関数の形 | def app(environ, start_response) | async def app(scope, receive, send) |
| 非同期 | 不可 | ネイティブ |
| プロトコル | HTTP のみ | HTTP、WebSocket、HTTP/2 |
| サーバー | gunicorn、uWSGI | uvicorn、daphne、hypercorn |
| 並行性モデル | プロセス / スレッド | イベントループ + コルーチン |
Django は両方サポートしています。wsgi.py、asgi.py という 2 つのエントリポイントが一緒にあります。
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 #
import asyncio
from django.http import JsonResponse
async def hello(request):
await asyncio.sleep(0.1)
return JsonResponse({"hello": "async"})def が async def に変わっただけです。ルーティングは同じ。
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] |
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 が中心パターンです。
⚠ 関係アクセスは依然として同期 #
async def post_detail(request, pk):
post = await Post.objects.aget(pk=pk)
author_name = post.author.name # SynchronousOnlyOperationORM オブジェクトの 外部キー lazy load は同期コード です。非同期コンテキストでそのまま呼ぶと SynchronousOnlyOperation 例外が出ます。
解決:
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 が起こりません。#3 の select_related / prefetch_related は非同期ではほぼ必須です。
外部 API の同時呼び出し — async の真価 #
非同期 view が輝く場面。外部サービス 3 つを 同時に 呼ぶページを見ます。
pip install httpximport 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 — 同期関数を非同期から呼ぶ
#
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 メソッドも内部的にこれを使っています。
@sync_to_async(thread_sensitive=True)
def db_work():
...thread_sensitive=True は 常に同じスレッド で実行 — 同期 ORM のトランザクションのようなスレッドローカルな状態を持つコードに必要です。デフォルト値は True。
async_to_sync — 非同期関数を同期から呼ぶ
#
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 #
ミドルウェアも非同期版を作れます。
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 つが標準です。
| uvicorn | daphne | hypercorn | |
|---|---|---|---|
| メンテナ | encode | django | pgjones |
| HTTP/2 | サポート | 非対応 | サポート |
| WebSocket | サポート | Channels の標準 | サポート |
| 速度 | 最速 | 普通 | 普通 |
| 推奨場面 | 一般的な ASGI | Channels (#6) | HTTP/2 が必要なとき |
uvicorn #
pip install "uvicorn[standard]"
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4プロダクションは gunicorn + uvicorn worker の組み合わせが定番です。
pip install gunicorn
gunicorn myproject.asgi:application \
-k uvicorn.workers.UvicornWorker \
--workers 4 --bind 0.0.0.0:8000gunicorn がプロセス管理 (再起動、シグナル、ロギング) を担当し、uvicorn が実際の ASGI 処理を行います。
daphne #
pip install daphne
daphne -b 0.0.0.0 -p 8000 myproject.asgi:applicationChannels (#6) の標準サーバー。
同期 view と非同期 view の共存 #
同じプロジェクトに 2 つが混ざっていても大丈夫です。Django が自動検出。
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 の一部は同期) | 最初から非同期 |
| 型ヒント | 補助的 | コア動作 |
| OpenAPI | DRF + 別途 | ビルトイン |
| 学習曲線 | 急 | なだらか |
| 場面 | フルサイト、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 で呼ぶパターンを使います。
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メソッド —aget、acreate、aupdate、async for - 関係アクセスは依然として同期 —
select_relatedで先に - 外部 API の同時呼び出し (
asyncio.gather) が非同期の真価 sync_to_async、async_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 らしく作る方法。