この記事は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)