初心者向け

【Django】シグナルについて

シグナルとはある特定のモデルが保存などの動作をした際、他のモデルに特定の動作をさせる仕組みです。

簡単に言うと保存などの動作を行った際、指定された関数をコールバックします。

動作の例を挙げます。

ECサイトのような通販サイトを作成したとき、例えばカードモデルと請求モデルがあったとします。

ECサイトですのでカートに商品を追加したとき、合計額を計算し直さなければなりません。

カートモデルに商品を追加した時にシグナルを発行して、請求モデルに合計額を計算し直して保存することができます。

※ 合計を計算する関数をコールバックするということです。

Djangoの動作をよく知っている方であればsaveメソッドをオーバーライドすれば良いのでは?と感じるかもしれません。

しかし、それではうまく処理しないとsaveメソッドを呼んだあとにさらにsaveメソッドが呼ばれる無限ループに陥る事があります。

そういった事を防ぎつつ、他のモデルの動作を促す機能です。

注意

シグナルは使いすぎるとどのシグナルでどのモデルが動作しているかわかりづらくなります。

使いすぎには注意してください。

Two Scope Django でも指摘されている内容です。

動作の流れ

実際のコードを見ていく前に、どのように実装していくかの流れを説明します。

  1. シグナルを受け取ったときに動作する関数を定義する
  2. シグナルを定義した関数に接続する

このような流れです。

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などでよく使われます。

はじめからコールバックする事を前提にしたライブラリもたくさんあります。

たとえば「記事のデータを取得した後にその一覧を表示する」といった場合、

  1. 記事のデータを取得する
  2. 一覧を表示する

というように処理を分け、「記事のデータを取得する」処理が終わった後に2の一覧を表示する関数を呼び出し(コールバックし)ます。

このようにすれば、データを取得した後自動的に表示処理に移ることができます。

コールバックというのはそのように、「ある特定のアクションの後に関数などを呼び出す」事をいいます。

コールバックされる関数の引数について

コールバックされる関数の引数ですが、それぞれシグナルによってキーワード引数が設定されています。

ドキュメントに引数の詳細が載っていますので参考にしてください。

コールバック関数の第一引数は、シグナルの種類を問わずsenderとなっています。シグナルを送ったクラスのことです。

第二引数以降はシグナルによって違います。

例えば、私がよく使うpre_savepost_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の名前付き引数にsenderBillDetailを設定しています。

このようにするとBillDetailモデルが保存されるときにこのシグナルが送信されるようになります。

BillDetailがシグナルの送り主、というわけです。

def post_detail_save(sender, instance, **kwargs)

コールバックされる関数post_detail_saveinstanceには実際に保存されるモデルが渡されてきます。繰り返しになりますが、この関数は接続(connect)するときにsender=BillDetailを指定していますのでBillDetailがデータを保存した後に保存されたデータがこのBillDetailのインスタンスとして渡ってくる、ということです。

この後は、そのinstanceを使って望みの処理をします。

この後はシグナルとは直接関係ありませんが、処理の内容を記述します。

BillDetailモデルはBillモデルにリレーションを張っているので、インスタンスが取得できればどの請求書の細目なのかはリレーションをたどれば分かります。

Billクラスには細目すべての合計を計算するメソッドを用意していますので呼び出します。

最後にBillクラスのインスタンスを保存をしてコールバックされる関数を抜けます。

このようにすると、細目が追加されたときに常に合計を再計算する事ができるようになります。

まとめ

Djangoのシグナルは上記のように、「ある特定のタイミングを引き金に、他の処理を差し込む」機能のことです。

シグナルを送る方・送られる方と考えるとややこしくなってきますが、

  • 送る方がモデル
  • 送られる方、つまりコールバックされるのは関数

このように考えると少しは整理できると思います。

-初心者向け
-,