Django中級 #3 Signals と Middleware
Django には モデル・ビューの正常フロー外 で起きるコードを差し込むツールが 2 種類あります。
- Signals — 「ある出来事が起きたとき」に反応するイベントシステム
- Middleware — すべてのリクエスト / レスポンスが通るパイプライン
両方とも強力ですが 乱用するとデバッグ地獄 に向かうツールです。今回は使い方とともに いつ使うべきでないか も併せて見ます。
#1 CBV、#2 ORM 中級 で見たツールが「正常フロー内のツール」だったとすれば、今回は そのフローを横断するツール です。
Signals — イベントシステム #
Django のシグナルは 送信者 (sender) → 受信者 (receiver) パターンです。一カ所で「この出来事が起きた」を発信すると、登録された受信者たちが非同期のように (実際は同期) 呼び出されます。
ビルトインシグナル — よく使うもの #
| シグナル | 発信時点 | よく使う場面 |
|---|---|---|
pre_save | モデル save() 直前 | スラッグ自動生成、正規化 |
post_save | save() 直後 | キャッシュ無効化、通知 |
pre_delete | delete() 直前 | 外部リソース整理 |
post_delete | delete() 直後 | キャッシュ無効化、ログ |
m2m_changed | M2M リレーション変更 | 権限・統計の更新 |
request_started / request_finished | リクエスト開始/終了 | グローバルログ |
user_logged_in / user_logged_out | 認証イベント | 最終ログイン時刻の更新 |
受信者の登録 — @receiver
#
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.text import slugify
from .models import Post
@receiver(post_save, sender=Post)
def post_post_save(sender, instance, created, **kwargs):
if created:
# 新しく作られた記事に対してのみ
send_notification_to_subscribers(instance)@receiver(signal, sender=Model) デコレータで関数をシグナルに紐付けます。ハンドラのシグネチャ:
sender— 発信者クラス (上の例ではPost)instance— 保存されたモデルインスタンスcreated—Trueなら新規作成、Falseなら更新**kwargs— シグナルごとに追加の引数が違う可能性があるため常に**kwargsで受ける
apps.py の ready() で import
#
シグナルモジュールは アプリのロード時点で 1 度 import されないと登録されません。apps.py がその出番。
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
from . import signals # 登録トリガーready() の中で import する理由は 循環 import とアプリのロード順序の問題を避けるため です。モデルがすべてロードされた時点でシグナルが登録されます。
自前のシグナル #
import django.dispatch
post_published = django.dispatch.Signal()
# 受信
@receiver(post_published)
def on_published(sender, post, **kwargs):
print(f"公開: {post.title}")from .signals import post_published
class PostUpdateView(UpdateView):
def form_valid(self, form):
response = super().form_valid(form)
if self.object.published:
post_published.send(sender=self.__class__, post=self.object)
return responseSignal() で作ったオブジェクトに .send(sender, **kwargs) で発信します。
pre_save — スラッグ自動生成
#
@receiver(pre_save, sender=Post)
def post_pre_save(sender, instance, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.title)保存直前にスラッグを埋めます。ただし このようなケースは通常モデルの save() のオーバーライドの方が適切 です。次の節で説明します。
シグナルの落とし穴 — いつ使うべきでないか #
シグナルは 遠くにある副作用 を生みやすいです。
# どこかに散らばった受信者たち…
@receiver(post_save, sender=Order)
def send_email(...): ...
@receiver(post_save, sender=Order)
def update_stats(...): ...
@receiver(post_save, sender=Order)
def call_external_api(...): ...Order.objects.create(...) 1 行を書いただけで どこで何が起きるのか コード 1 か所だけ見ても分かりません。シグナルが複数のファイルに散らばっているからです。
代案 1 — モデルメソッド #
class Post(models.Model):
...
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def publish(self):
self.published = True
self.published_at = timezone.now()
self.save()
notify_subscribers(self)post.publish() 1 行を見れば 何が起きるかそのメソッドだけ見れば済みます。シグナルより追跡が楽です。
代案 2 — マネージャ / サービス関数 #
def publish_post(post: Post) -> None:
post.published = True
post.published_at = timezone.now()
post.save()
notify_subscribers(post)
invalidate_homepage_cache()ドメインロジックを明示的な関数に集めます。テストも楽で、呼び出し経路も明確。
シグナルが本当に向く場面 #
| ケース | シグナル OK? | 理由 |
|---|---|---|
他アプリが発信するモデル (Django ビルトインの User など) にフック | ✅ | そのモデルを修正できない |
| 外部ライブラリのモデルにフック | ✅ | 同じ理由 |
| 同じアプリ内の副作用 | ❌ | モデルメソッド/サービスが明示的 |
| トランザクション内の純粋な永続化作業 | ✅ | 一貫性をシグナルが保証 |
原則: 自分のコードを自分の手で直せるならシグナルより明示的な呼び出しが答え。シグナルは「他人のモデルにフックを掛ける」ツールと考えてください。
トランザクションとシグナルを一緒に使うパターン (transaction.on_commit、post_save + atomic) は 上級 #5 で詳しく。
Middleware — リクエスト/レスポンスパイプライン #
ミドルウェアは すべてのリクエストとレスポンスが通るパイプライン です。認証、セッション、CSRF のような全域的な関心事がここに住みます。
動作モデル #
ブラウザ
↓
SecurityMiddleware
↓
SessionMiddleware
↓
CommonMiddleware
↓
CsrfViewMiddleware
↓
AuthenticationMiddleware
↓
MessageMiddleware
↓
View ← ここでレスポンス生成
↑
MessageMiddleware
↑
... (逆順で再び通過)
↑
ブラウザ入るときは上から下へ、出るときは下から上へ。 玉ねぎの皮のように包みます。
settings.py に登録
#
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]順序が重要です。たとえば AuthenticationMiddleware は SessionMiddleware より 後 に来なければなりません (セッションを読んでユーザーを認証するから)。
ビルトインの主要ミドルウェア #
| ミドルウェア | 何をするか |
|---|---|
SecurityMiddleware | HTTPS リダイレクト、HSTS、X-Content-Type-Options など |
SessionMiddleware | request.session を有効化 (#5) |
CommonMiddleware | URL 正規化 (スラッシュ追加)、統計ヘッダ |
CsrfViewMiddleware | CSRF トークン検証 |
AuthenticationMiddleware | request.user を有効化 (#4) |
MessageMiddleware | flash メッセージ (#5) |
XFrameOptionsMiddleware | clickjacking 対策 (X-Frame-Options) |
ビルトインは切らないのが普通の答えです。セキュリティに直結するものが多いです。
ミドルウェアの作成 — クラス形式 #
import time
import logging
logger = logging.getLogger(__name__)
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.perf_counter()
response = self.get_response(request)
elapsed = (time.perf_counter() - start) * 1000
response["X-Render-Time"] = f"{elapsed:.1f}ms"
logger.info(f"{request.method} {request.path} {elapsed:.1f}ms")
return response構造:
__init__(self, get_response)— アプリ起動時に 1 度だけ呼ばれる。get_responseは次のミドルウェア (またはビュー) を呼ぶための関数__call__(self, request)— リクエストごとに呼ばれるself.get_response(request)の前のコード → リクエストが入るときself.get_response(request)の後のコード → レスポンスが出るとき
MIDDLEWARE = [
...
"myapp.middleware.TimingMiddleware",
]フックメソッド — process_view、process_exception、process_template_response
#
追加のフックが必要ならメソッドをさらに定義します。
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def process_view(self, request, view_func, view_args, view_kwargs):
# ビュー関数が呼ばれる直前。None を返せば正常進行
request._view_func_name = view_func.__name__
def process_exception(self, request, exception):
# ビューで処理されない例外が出たとき
logger.error(f"未処理の例外: {exception}", exc_info=True)
# None を返す → 他のハンドラに委譲 / HttpResponse → レスポンスとして使う
def process_template_response(self, request, response):
# TemplateResponse のときレンダ直前
return responseミドルウェアでよく解く問題 #
- リクエスト ID 発行 — 分散トレース用
X-Request-IDヘッダ付与 - 多言語/タイムゾーン —
Accept-Languageを見て有効言語を決定 - メンテナンスモード — 特定経路以外は 503 レスポンス
- シンプルな rate limit — キャッシュ + IP ベース (大トラフィックには別ツール)
- 共通レスポンスヘッダ — セキュリティヘッダ、CORS など
ミドルウェア vs デコレータ vs シグナル — いつ何を #
3 つのツールの使い分けが混乱しがちです。
| ツール | 用途 |
|---|---|
| デコレータ (または Mixin) | 特定のビューだけにかける横断的関心事 (例: @login_required) |
| ミドルウェア | すべてのリクエストにかけるグローバルな関心事 (例: リクエスト ID、セキュリティヘッダ) |
| シグナル | モデル・認証のようなドメインイベントのフック (自分のコードなら明示呼び出しが優先) |
判断基準:
- 特定のビューだけ → デコレータ / Mixin
- すべてのリクエスト → ミドルウェア
- モデル保存後の処理 → 同じアプリならメソッド/サービス、外部モデルならシグナル
小さな実戦例 — アクティブユーザーの最終活動時刻 #
from django.utils import timezone
class LastSeenMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.user.is_authenticated:
User = request.user.__class__
User.objects.filter(pk=request.user.pk).update(
last_seen_at=timezone.now()
)
return responseUser.objects.filter(...).update(...) で ユーザーオブジェクトを再 save せずに 更新します (#2 のパターン)。リクエストごとの負担を減らすには、5 分に 1 度だけ更新するような throttle を追加してください。
非同期ミドルウェアを短く #
Django 4.0+ からミドルウェアを非同期でも書けるようになりました。本格的な非同期ビューは 上級 #1 で扱いますが、ミドルウェアは同期/非同期両対応を作れます。
from asgiref.sync import iscoroutinefunction
class HybridMiddleware:
sync_capable = True
async_capable = True
def __init__(self, get_response):
self.get_response = get_response
self.async_mode = iscoroutinefunction(get_response)
def __call__(self, request):
if self.async_mode:
return self.__acall__(request)
return self.get_response(request)
async def __acall__(self, request):
return await self.get_response(request)詳細パターンは 上級 #1 で。
まとめ #
今回押さえたもの:
- Signals — 送信者/受信者のイベントシステム
- ビルトインシグナル:
pre_save、post_save、pre_delete、post_delete、m2m_changed、認証イベント @receiver(signal, sender=Model)で登録、apps.pyのready()で import- シグナルの落とし穴 — 遠くにある副作用、デバッグが困難。自分のモデルならメソッド/サービスが優先
- Middleware — リクエスト/レスポンスパイプライン、玉ねぎの皮のフロー
- ビルトインの主要 (
Security、Session、Csrf、Authentication、Message) - ミドルウェアの形式:
__init__(get_response)、__call__(request)、追加でprocess_view/process_exception - 3 つのツールの使い分け: 特定のビュー → デコレータ/Mixin、すべてのリクエスト → ミドルウェア、ドメインイベント → シグナル (自分のコードなら明示呼び出しが優先)
次回 (#4 ユーザー / 権限) では、基礎 #7 のビルトイン認証の上に カスタム user model、permission、group を積み上げます。プロジェクト開始時点で決めるべき決定も併せて。