Django中級 #3 Signals と Middleware

読了 8分

Django には モデル・ビューの正常フロー外 で起きるコードを差し込むツールが 2 種類あります。

  • Signals — 「ある出来事が起きたとき」に反応するイベントシステム
  • Middleware — すべてのリクエスト / レスポンスが通るパイプライン

両方とも強力ですが 乱用するとデバッグ地獄 に向かうツールです。今回は使い方とともに いつ使うべきでないか も併せて見ます。

#1 CBV#2 ORM 中級 で見たツールが「正常フロー内のツール」だったとすれば、今回は そのフローを横断するツール です。

Signals — イベントシステム #

Django のシグナルは 送信者 (sender) → 受信者 (receiver) パターンです。一カ所で「この出来事が起きた」を発信すると、登録された受信者たちが非同期のように (実際は同期) 呼び出されます。

ビルトインシグナル — よく使うもの #

シグナル発信時点よく使う場面
pre_saveモデル save() 直前スラッグ自動生成、正規化
post_savesave() 直後キャッシュ無効化、通知
pre_deletedelete() 直前外部リソース整理
post_deletedelete() 直後キャッシュ無効化、ログ
m2m_changedM2M リレーション変更権限・統計の更新
request_started / request_finishedリクエスト開始/終了グローバルログ
user_logged_in / user_logged_out認証イベント最終ログイン時刻の更新

受信者の登録 — @receiver #

blog/signals.py
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 — 保存されたモデルインスタンス
  • createdTrue なら新規作成、False なら更新
  • **kwargs — シグナルごとに追加の引数が違う可能性があるため常に **kwargs で受ける

apps.pyready() で import #

シグナルモジュールは アプリのロード時点で 1 度 import されないと登録されません。apps.py がその出番。

blog/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 とアプリのロード順序の問題を避けるため です。モデルがすべてロードされた時点でシグナルが登録されます。

自前のシグナル #

blog/signals.py — カスタムシグナル
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 response

Signal() で作ったオブジェクトに .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_commitpost_save + atomic) は 上級 #5 で詳しく。

Middleware — リクエスト/レスポンスパイプライン #

ミドルウェアは すべてのリクエストとレスポンスが通るパイプライン です。認証、セッション、CSRF のような全域的な関心事がここに住みます。

動作モデル #

リクエストフロー
ブラウザ
SecurityMiddleware
SessionMiddleware
CommonMiddleware
CsrfViewMiddleware
AuthenticationMiddleware
MessageMiddleware
View                    ← ここでレスポンス生成
MessageMiddleware
... (逆順で再び通過)
ブラウザ

入るときは上から下へ、出るときは下から上へ。 玉ねぎの皮のように包みます。

settings.py に登録 #

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",
]

順序が重要です。たとえば AuthenticationMiddlewareSessionMiddleware より に来なければなりません (セッションを読んでユーザーを認証するから)。

ビルトインの主要ミドルウェア #

ミドルウェア何をするか
SecurityMiddlewareHTTPS リダイレクト、HSTS、X-Content-Type-Options など
SessionMiddlewarerequest.session を有効化 (#5)
CommonMiddlewareURL 正規化 (スラッシュ追加)、統計ヘッダ
CsrfViewMiddlewareCSRF トークン検証
AuthenticationMiddlewarerequest.user を有効化 (#4)
MessageMiddlewareflash メッセージ (#5)
XFrameOptionsMiddlewareclickjacking 対策 (X-Frame-Options)

ビルトインは切らないのが普通の答えです。セキュリティに直結するものが多いです。

ミドルウェアの作成 — クラス形式 #

myapp/middleware.py
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) の後のコード → レスポンスが出るとき
settings.py に追加
MIDDLEWARE = [
    ...
    "myapp.middleware.TimingMiddleware",
]

フックメソッド — process_viewprocess_exceptionprocess_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、セキュリティヘッダ)
シグナルモデル・認証のようなドメインイベントのフック (自分のコードなら明示呼び出しが優先)

判断基準:

  1. 特定のビューだけ → デコレータ / Mixin
  2. すべてのリクエスト → ミドルウェア
  3. モデル保存後の処理 → 同じアプリならメソッド/サービス、外部モデルならシグナル

小さな実戦例 — アクティブユーザーの最終活動時刻 #

myapp/middleware.py
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 response

User.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_savepost_savepre_deletepost_deletem2m_changed、認証イベント
  • @receiver(signal, sender=Model) で登録、apps.pyready() で import
  • シグナルの落とし穴 — 遠くにある副作用、デバッグが困難。自分のモデルならメソッド/サービスが優先
  • Middleware — リクエスト/レスポンスパイプライン、玉ねぎの皮のフロー
  • ビルトインの主要 (SecuritySessionCsrfAuthenticationMessage)
  • ミドルウェアの形式: __init__(get_response)__call__(request)、追加で process_view/process_exception
  • 3 つのツールの使い分け: 特定のビュー → デコレータ/Mixin、すべてのリクエスト → ミドルウェア、ドメインイベント → シグナル (自分のコードなら明示呼び出しが優先)

次回 (#4 ユーザー / 権限) では、基礎 #7 のビルトイン認証の上に カスタム user model、permission、group を積み上げます。プロジェクト開始時点で決めるべき決定も併せて。

X