基本

Djangoシグナル入門|モデルのイベントに自動で処理を実行する

Django シグナル Python

Djangoシグナル入門
モデルのイベントに自動で処理を実行する

Djangoのシグナルシステムを解説。pre_save/post_save、@receiver、カスタムシグナル、実践的な活用パターンまで学べます。

こんな人向けの記事です

  • Djangoのシグナルの仕組みを理解したい
  • モデル保存時に自動処理を実行したい
  • カスタムシグナルを作成したい

Step 1シグナルとは ― イベント駆動のフック

Djangoのシグナル(Signal)は、アプリケーション内で特定のイベントが発生したとき、別の処理を自動的に実行する仕組みです。いわゆるオブザーバーパターン(Observer Pattern)の実装であり、「送信者(sender)」と「受信者(receiver)」を疎結合に保ちながら連携できます。

用語説明
シグナル(Signal)イベントの種類を表すオブジェクト
送信者(Sender)シグナルを発行する側(例: モデルクラス)
受信者(Receiver)シグナルを受け取って処理を実行する関数
接続(Connect)シグナルと受信者を紐づける操作

例えば「ユーザーが作成されたらプロフィールも自動で作る」「記事が削除されたらキャッシュをクリアする」など、モデルの保存・削除に連動した処理を、モデル自体のコードを変更せずに追加できます。

なぜシグナルを使うのか?

モデルの save() メソッドをオーバーライドしても同じことは実現できます。しかしシグナルなら、他のアプリから後付けで処理を差し込めるのが最大のメリットです。再利用可能なアプリを作る際に特に有効です。

Step 2組み込みシグナル(pre_save / post_save / pre_delete / post_delete)

Djangoにはモデル操作に関する主要なシグナルが用意されています。最もよく使われる4つを確認しましょう。

シグナル発火タイミング主な用途
pre_saveモデルの save() 実行バリデーション、フィールドの自動補完
post_saveモデルの save() 実行関連レコードの作成、通知送信
pre_deleteモデルの delete() 実行関連データのバックアップ、依存チェック
post_deleteモデルの delete() 実行ファイル削除、キャッシュクリア

その他の組み込みシグナル

シグナル説明
m2m_changedManyToManyField の関連が変更されたとき
request_startedHTTPリクエスト処理の開始時
request_finishedHTTPリクエスト処理の終了時
got_request_exceptionリクエスト処理中に例外が発生したとき

シグナルのインポート

from django.db.models.signals import (
    pre_save,
    post_save,
    pre_delete,
    post_delete,
    m2m_changed,
)
from django.core.signals import request_started, request_finished

各シグナルに渡される引数

post_save の受信関数には以下の引数が渡されます。

引数説明
senderモデルクラスシグナルを発行したモデル
instanceモデルインスタンス保存されたオブジェクト
createdboolTrue なら新規作成、False なら更新
**kwargsdictその他のキーワード引数

pre_save には created 引数がない

pre_save の時点ではまだDBに保存されていないため、created 引数は渡されません。新規作成かどうかを判定するには instance.pk is None をチェックします。

Step 3シグナルの接続方法(@receiver デコレータ)

シグナルに処理を接続する方法は2つあります。推奨されるのは @receiver デコレータです。

方法1: @receiver デコレータ(推奨)

myapp/signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def user_saved(sender, instance, created, **kwargs):
    if created:
        print(f"新しいユーザーが作成されました: {instance.username}")
    else:
        print(f"ユーザーが更新されました: {instance.username}")

方法2: connect() メソッド

myapp/signals.py

from django.db.models.signals import post_save
from django.contrib.auth.models import User

def user_saved(sender, instance, created, **kwargs):
    if created:
        print(f"新しいユーザーが作成されました: {instance.username}")

# connect() で手動接続
post_save.connect(user_saved, sender=User)

シグナルファイルの読み込み設定

シグナルを定義しただけでは動きません。AppConfig の ready() メソッドで読み込む必要があります。

myapp/apps.py

from django.apps import AppConfig

class MyappConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'myapp'

    def ready(self):
        import myapp.signals  # ここでシグナルを読み込む

ready() を忘れるとシグナルが動かない

最も多いミスが ready() でのインポート忘れです。シグナルを定義したのに動かないときは、まず apps.pyready() を確認してください。また __init__.pydefault_app_config が正しく設定されていることも確認しましょう。

推奨のファイル構成

ディレクトリ構成

myapp/
├── __init__.py
├── apps.py          # ready() でシグナルを読み込む
├── models.py        # モデル定義
├── signals.py       # シグナルハンドラをまとめる
├── views.py
└── admin.py

sender を省略すると?

@receiver(post_save) のように sender を省略すると、すべてのモデルの保存時にシグナルが発火します。パフォーマンスに影響するため、通常は sender を指定しましょう。

Step 4カスタムシグナルの作成

Django組み込みのシグナルだけでなく、独自のシグナルを定義してアプリケーション固有のイベントを通知できます。

カスタムシグナルの定義

myapp/signals.py

import django.dispatch

# カスタムシグナルの定義
order_completed = django.dispatch.Signal()  # 注文完了シグナル
payment_received = django.dispatch.Signal()  # 支払い受領シグナル

シグナルの送信

myapp/views.py

from myapp.signals import order_completed

def complete_order(request, order_id):
    order = Order.objects.get(id=order_id)
    order.status = 'completed'
    order.save()

    # カスタムシグナルを送信
    order_completed.send(
        sender=Order,
        order=order,
        user=request.user,
    )
    return redirect('order_detail', order_id=order.id)

シグナルの受信

notifications/signals.py

from django.dispatch import receiver
from myapp.signals import order_completed

@receiver(order_completed)
def send_order_confirmation(sender, order, user, **kwargs):
    """注文完了メールを送信"""
    send_mail(
        subject=f'注文 #{order.id} が完了しました',
        message=f'{user.username} 様、ご注文ありがとうございます。',
        from_email='shop@example.com',
        recipient_list=[user.email],
    )

@receiver(order_completed)
def update_inventory(sender, order, **kwargs):
    """在庫を更新"""
    for item in order.items.all():
        item.product.stock -= item.quantity
        item.product.save()

send() と send_robust() の違い

メソッド例外発生時の挙動用途
send()例外がそのまま伝播する開発環境、エラーを即座に検知したい場合
send_robust()例外をキャッチし、戻り値として返す本番環境、1つの受信者の失敗で他を止めたくない場合

send_robust() の使用例

# send_robust() は例外が起きてもすべての受信者を実行する
results = order_completed.send_robust(
    sender=Order,
    order=order,
    user=request.user,
)

# 各受信者の結果を確認
for receiver_func, response in results:
    if isinstance(response, Exception):
        print(f"エラー: {receiver_func.__name__} - {response}")
    else:
        print(f"成功: {receiver_func.__name__}")

providing_args は廃止済み

Django 3.1 以前では Signal(providing_args=['order', 'user']) のように引数を宣言していましたが、Django 4.0 で削除されました。現在は Signal() のみで定義し、引数はドキュメントやコメントで示します。

Step 5シグナルの実行順序と注意点

シグナルは便利ですが、使い方を誤るとバグやパフォーマンス低下の原因になります。重要な注意点を押さえておきましょう。

実行順序

同じシグナルに複数の受信者が接続されている場合、実行順序は保証されません。接続された順に実行される傾向はありますが、仕様として保証はされていません。

実行順序に依存してはいけない

受信者Aの結果を受信者Bが前提とするような設計は避けてください。各受信者は独立して動作するように実装しましょう。順序が重要な場合は、1つの受信者内で順番に処理を呼び出します。

トランザクションとの関係

トランザクション完了後に実行する

from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Order)
def notify_order_saved(sender, instance, created, **kwargs):
    # post_save はトランザクションのコミット前に発火する可能性がある
    # 外部APIの呼び出しなどはトランザクション完了後に行う
    transaction.on_commit(
        lambda: send_notification(instance)
    )

post_save はコミット前に発火する

post_save はモデルの save() 完了後に発火しますが、トランザクションのコミット前です。外部APIの呼び出しやメール送信など、ロールバックされては困る処理は transaction.on_commit() で囲みましょう。

よくある落とし穴

問題原因対策
シグナルが動かないready() で import していないapps.pyready() を確認
無限ループシグナル内で save() を呼んでいるupdate() を使うか、フラグで制御
2回発火するシグナルが2回接続されているdispatch_uid を指定する
テストで予期しない動作シグナルが意図せず発火テスト時にシグナルを一時無効化
パフォーマンス低下シグナル内で重い処理をしている非同期タスク(Celery等)に委譲

無限ループの防止

無限ループを防ぐパターン

@receiver(post_save, sender=Article)
def update_word_count(sender, instance, **kwargs):
    # NG: save() を呼ぶと post_save が再発火 → 無限ループ
    # instance.word_count = len(instance.content.split())
    # instance.save()

    # OK: update() はシグナルを発火しない
    Article.objects.filter(pk=instance.pk).update(
        word_count=len(instance.content.split())
    )

dispatch_uid で重複接続を防ぐ

dispatch_uid の指定

# dispatch_uid を指定すると、同じIDで2回以上接続されない
@receiver(post_save, sender=User, dispatch_uid="create_user_profile")
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

Step 6実践例 ― プロフィール自動作成・ログ記録

ここまでの知識を使って、実際のプロジェクトでよく使われるパターンを実装してみましょう。

実践例1: ユーザー作成時にプロフィールを自動作成

accounts/models.py

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True, default='')
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'{self.user.username} のプロフィール'

accounts/signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile

@receiver(post_save, sender=User, dispatch_uid="create_user_profile")
def create_profile(sender, instance, created, **kwargs):
    """ユーザー作成時にプロフィールを自動作成"""
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User, dispatch_uid="save_user_profile")
def save_profile(sender, instance, **kwargs):
    """ユーザー保存時にプロフィールも保存"""
    if hasattr(instance, 'profile'):
        instance.profile.save()

accounts/apps.py

from django.apps import AppConfig

class AccountsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'accounts'

    def ready(self):
        import accounts.signals

実践例2: モデル変更のログ記録

audit/models.py

from django.db import models

class AuditLog(models.Model):
    ACTION_CHOICES = [
        ('create', '作成'),
        ('update', '更新'),
        ('delete', '削除'),
    ]
    model_name = models.CharField(max_length=100)
    object_id = models.IntegerField()
    action = models.CharField(max_length=10, choices=ACTION_CHOICES)
    changes = models.JSONField(default=dict, blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-timestamp']

    def __str__(self):
        return f'{self.model_name} #{self.object_id} {self.action}'

audit/signals.py

from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import AuditLog

# 監査対象のモデル一覧
AUDITED_MODELS = []

def audit_model(model_class):
    """デコレータ: モデルを監査対象に登録"""
    AUDITED_MODELS.append(model_class)
    return model_class

@receiver(post_save)
def log_save(sender, instance, created, **kwargs):
    if sender not in AUDITED_MODELS:
        return

    AuditLog.objects.create(
        model_name=sender.__name__,
        object_id=instance.pk,
        action='create' if created else 'update',
    )

@receiver(post_delete)
def log_delete(sender, instance, **kwargs):
    if sender not in AUDITED_MODELS:
        return

    AuditLog.objects.create(
        model_name=sender.__name__,
        object_id=instance.pk,
        action='delete',
    )

myapp/models.py(監査対象のモデル)

from django.db import models
from audit.signals import audit_model

@audit_model
class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    # ... このモデルの作成・更新・削除が自動でログに記録される

実践例3: テスト時にシグナルを無効化

tests/test_user.py

from django.test import TestCase
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from accounts.signals import create_profile

class UserTestCase(TestCase):
    def setUp(self):
        # テスト中はプロフィール自動作成を無効化
        post_save.disconnect(create_profile, sender=User)

    def tearDown(self):
        # テスト後に再接続
        post_save.connect(create_profile, sender=User)

    def test_user_creation_without_profile(self):
        user = User.objects.create_user('testuser', 'test@example.com', 'pass')
        self.assertFalse(hasattr(user, 'profile'))

シグナル活用のチェックリスト

  • signals.py にシグナルハンドラをまとめている
  • apps.pyready() でインポートしている
  • dispatch_uid を指定して重複接続を防いでいる
  • シグナル内で save() を呼ばず無限ループを避けている
  • 外部API呼び出しは transaction.on_commit() で囲んでいる
  • テスト時はシグナルの無効化を検討している

まとめ

  • シグナルはDjangoのイベント駆動フックで、モデルの保存・削除に連動した処理を疎結合に実装できる
  • 組み込みシグナル: pre_save / post_save / pre_delete / post_delete が主要な4つ
  • @receiver デコレータで接続し、apps.pyready() で読み込む
  • カスタムシグナルdjango.dispatch.Signal() で定義し、send() / send_robust() で発火する
  • 注意点: 実行順序は保証されない、無限ループに注意、transaction.on_commit() を活用する
  • 実践: プロフィール自動作成、監査ログ、テスト時の無効化が代表的なパターン