シグナルとはある特定のモデルが保存などの動作をした際、他のモデルに特定の動作をさせる仕組みです。
簡単に言うと保存などの動作を行った際、指定された関数をコールバックします。
動作の例を挙げます。
ECサイトのような通販サイトを作成したとき、例えばカードモデルと請求モデルがあったとします。
ECサイトですのでカートに商品を追加したとき、合計額を計算し直さなければなりません。
カートモデルに商品を追加した時にシグナルを発行して、請求モデルに合計額を計算し直して保存することができます。
※ 合計を計算する関数をコールバックするということです。
Djangoの動作をよく知っている方であればsave
メソッドをオーバーライドすれば良いのでは?と感じるかもしれません。
しかし、それではうまく処理しないとsave
メソッドを呼んだあとにさらにsave
メソッドが呼ばれる無限ループに陥る事があります。
そういった事を防ぎつつ、他のモデルの動作を促す機能です。
注意
シグナルは使いすぎるとどのシグナルでどのモデルが動作しているかわかりづらくなります。
使いすぎには注意してください。
Two Scope Django
でも指摘されている内容です。
動作の流れ
実際のコードを見ていく前に、どのように実装していくかの流れを説明します。
- シグナルを受け取ったときに動作する関数を定義する
- シグナルを定義した関数に接続する
このような流れです。
1 で定義した関数は複数の呼び出しを受けることがありますが、本稿では割愛します。
実際のコード
どこに書くのか
シグナルをどこに書くのかは特に決まっていません。
私の場合、ちょっとしたものであればmodels.py
に記述してしまいます。
実装自体はモデルにしている事も多いためメソッドを呼ぶだけの事がほとんどで、これで事足りることが多いです。
すこし多めに記述するときは、signals.py
というファイルを作ってそこにコールバックを記述します。(わかりやすさのため、はじめからこのファイルを作成する人もいらっしゃるようです)
そして、models.py
に後述するconnect
を使用した接続を明記します。
流れの確認
では、実際のコードを記述していきます。
ここではドキュメントを参考に、コードを記述していきます。
ドキュメントの内容ではrequest_finished
というシグナルを使用しています。
これは、ユーザーからのリクエストが終了したタイミングで発行されるシグナルです。
要は、誰かがページにアクセスするたびに発行されるシグナルということであり、ページにアクセスがあるたびにコールバックされるということです。
まずはシグナルを受け取ったときに動作する関数を定義します。
これはコールバックされる関数と考えていただいて問題ないです。
def my_callback(sender, **kwargs):
print('Request Finished')
そして、この関数にシグナルを接続します。
from django.core.signals import request_finished
request_finished.connect(my_callback)
request_finished
にはconnect
メソッドがありますので、このメソッドを使って関数を接続します。
コールバックとは
コールバックをご存じない方もこの記事を読むかもしれませんので説明します。
コールバックというのは、ある特定のアクションの後に関数などを呼び出すことをいいます。
call + back
ですので、後で呼ぶというわけですね。
本稿の内容からは外れますが、JavaScript
などでよく使われます。
はじめからコールバックする事を前提にしたライブラリもたくさんあります。
たとえば「記事のデータを取得した後にその一覧を表示する」といった場合、
- 記事のデータを取得する
- 一覧を表示する
というように処理を分け、「記事のデータを取得する」処理が終わった後に2の一覧を表示する関数を呼び出し(コールバックし)ます。
このようにすれば、データを取得した後自動的に表示処理に移ることができます。
コールバックというのはそのように、「ある特定のアクションの後に関数などを呼び出す」事をいいます。
コールバックされる関数の引数について
コールバックされる関数の引数ですが、それぞれシグナルによってキーワード引数が設定されています。
ドキュメントに引数の詳細が載っていますので参考にしてください。
コールバック関数の第一引数は、シグナルの種類を問わずsender
となっています。シグナルを送ったクラスのことです。
第二引数以降はシグナルによって違います。
例えば、私がよく使うpre_save
やpost_save
では、実際に保存されるモデルのインスタンスが入っています。
私が記述した実際のコード
自分が記述したコードを参考にして解説します。
※ このコードは今は使っていません
まずはモデルからです。実際は複数に分かれていますが、説明のため一息に記述します。
また、適宜省略します。
なお、内容は請求書発行システムの一部です。
from django.db.models.signals import post_save
class Bill(models.Model):
# 省略
all_total = models.IntegerField()
def calc_total(self):
# ここですべての明細の合計を計算する
class BillDetail(models.Model):
bill = models.ForeignKey(Bill, on_delete=models.CASCADE)
total = models.IntegerField()
# 省略
def post_detail_save(sender, instance, **kwargs):
instance.bill.calc_total()
instance.bill.save() # 計算後なので保存する
post_save.connect(post_save_detail, sender=BillDetail)
このようになっています。
connect
の名前付き引数にsender
をBillDetail
を設定しています。
このようにするとBillDetail
モデルが保存されるときにこのシグナルが送信されるようになります。
BillDetail
がシグナルの送り主、というわけです。
def post_detail_save(sender, instance, **kwargs)
コールバックされる関数post_detail_save
のinstance
には実際に保存されるモデルが渡されてきます。繰り返しになりますが、この関数は接続(connect
)するときにsender=BillDetail
を指定していますのでBillDetail
がデータを保存した後に保存されたデータがこのBillDetail
のインスタンスとして渡ってくる、ということです。
この後は、そのinstance
を使って望みの処理をします。
この後はシグナルとは直接関係ありませんが、処理の内容を記述します。
BillDetail
モデルはBill
モデルにリレーションを張っているので、インスタンスが取得できればどの請求書の細目なのかはリレーションをたどれば分かります。
Bill
クラスには細目すべての合計を計算するメソッドを用意していますので呼び出します。
最後にBill
クラスのインスタンスを保存をしてコールバックされる関数を抜けます。
このようにすると、細目が追加されたときに常に合計を再計算する事ができるようになります。
まとめ
Djangoのシグナルは上記のように、「ある特定のタイミングを引き金に、他の処理を差し込む」機能のことです。
シグナルを送る方・送られる方と考えるとややこしくなってきますが、
- 送る方がモデル
- 送られる方、つまりコールバックされるのは関数
このように考えると少しは整理できると思います。