LoginSignup
9
6

More than 3 years have passed since last update.

Djangoシグナルハンドラの使い所

Last updated at Posted at 2019-12-14

この記事はgumi Inc. Advent Calendar 2019の12/14の記事です。

シグナルとは

何らかのイベントが発生した場合に、同時実行している他のプロセスに通知を送り、受信したプロセスは通知に応じた関数を実行するシグナルという仕組みがあります。
UNIXなどでは例外や強制終了といったイベントをキャッチして実行中のプロセスに割り込んで停止するというようなことに使われますが、Djangoフレームワークのシグナルは基本的にメインスレッド内でしか通信しないため、アプリケーション中の特定のイベントをキャッチして後続の処理に繋げる目的で使われます。

Djangoフレームワークでは以下の種類のシグナルがサポートされています。
HTTPリクエストやレコードの保存等の直前直後に呼び出されることから、共通処理を実装するのに向いています。

種類 コールタイミング 内容
request_started HTTPリクエスト処理の開始時 特になし
request_finished HTTPリクエスト処理の終了時 特になし
pre_save インスタンスのsave()実行直前 保存予定のインスタンス
post_save インスタンスのsave()直後 保存の完了したインスタンス
pre_delete インスタンスのdelete()実行直前 削除予定のインスタンス
post_delete インスタンスのdelete()実行直後 削除の完了したインスタンス

request_started

DjangoのアプリケーションがHTTPリクエストを受け付けた時に呼び出されるシグナルです。
受け取り方としてはデコレータで後続処理を行う関数をラップするか、signal.connectの引数で後続処理の関数を呼び出す方法があります。

from django.core.signals import request_started

@receiver(request_started)
def do_something(**kwargs):
    ...
from django.core import signals

signals.request_started.connect(do_something)

引数のsenderにHTTPリクエストを処理するハンドラークラスを受け取ります。
本来であればアプリケーションのコードを汚すことなくこの仕組みを利用してリクエストログの出力等に使いたいところですが、残念ながらシグナル自体はHTTPリクエストの情報を受け取らないので出来ません。
リクエスト毎に初期化したいローカルキャッシュのトリガー等に使う感じでしょうか。

request_finished

こちらはクライアントにレスポンスを返す直前に呼ばれるシグナルです。
request_started同様にHTTPリクエストの情報を受け取らないので用途は良しなに。

pre_save

Djangoのモデルインスタンスのsave()メソッド実行直前に呼び出されるシグナルです。
senderにインスタンスのクラスを、instanceに保存直前のインスタンスを保持するので事前処理のようなものをモデルクラス毎に書かずとも実装することが出来ます。

例) トランザクションの中で実行されているか確認
from django.db.models.signals import pre_save
from django.db import transaction


@receiver(pre_save, **kwargs)
def is_in_transaction(**kwargs):
    if not transaction.is_managed():
        raise Exception()

尚、レシーバのsenderの引数に特定のクラスを指定することで、そのクラスからのシグナルのみ選択的に受信することが出来ます。

@receiver(pre_save, sender=User, **kwargs)
(do something)

post_save

こちらはDjangoモデルインスタンスのsave()メソッドの実行直後に呼び出されるシグナルです。
同じくsenderにクラスを、instanceにsave()後のインスタンスを受け取るので、ログや通知などの事後処理を行うのに便利です。

例) save()した結果をログに出力
import logging
from django.db.models.signals import post_save


@receiver(post_save, **kwargs)
def query_log(instance, **kwargs):
    modified_ref = dict()
    for field in instance._meta.fields:
        modified_ref[field.name] = getattr(instance, field.name)

    logger.getLogger(instance.__class__.__name__)
    logger.info(modifed_ref)
9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6