【Django】モデル保存時に追加処理を実行する Signals(post_save / pre_save)実装ガイド

スポンサードリンク



Django · Signals · モデル保存時に追加処理

存時に追加処理を入れたい(レコード作成と同時に通知メール、関連レコード自動生成、キャッシュ更新等)場面は実装あるあるです。Django ではこの「保存後フック」を シグナル(post_save / pre_save / post_delete 等) で実装するのが定石。本記事ではシグナルの基本、適切な配置場所(signals.py)、apps.py での接続、よくある落とし穴(無限ループ・バルク操作の罠)まで解説します。

この記事では、djangoのシグナル機能を使った事例を紹介します。

具体的には、Djangoのあるテーブルレコードを新規登録した場合に、該当テーブルを外部参照しているようなテーブル側にも関連するレコードを自動的に登録させるといった事例でDjangoシグナルの使い方を説明したいと思います。

user@sinyblog:~/article 01_django.mdDjangoシグナルとは?

Django公式サイトには以下のように記載されています。

Djangoには「シグナルディスパッチャ」が含まれています。これにより、フレームワークの別の場所でアクションが発生したときに、切り離されたアプリケーションに通知することができます。簡単に言うと、シグナルによって、特定の送信者が一連の受信者に何らかのアクションが実行されたことを通知できます。

 

これだけだとよくわかりづらいのですが、例えば、あるテーブルのレコードが変更されたことをシグナルとして通知メールを送信するといった使い方ができるようです。

私もよく使い方がわからなかったのですが、具体的な事例で試してみましたので、実装方法をご紹介します。

user@sinyblog:~/article 02_section_2.mdシグナルの実装例

今回は、以下のような事例で実装してみました。

ブログに投稿記事を新規登録すると、関連するいいねテーブルのレコードを自動登録させる。

テーブルのモデルは以下のような構成です。

Postテーブルのtitleを外部キーとしたいいねテーブルを実装するものとします。

記事毎にいいねをカウントするレコードがGoodテーブルに存在しているという感じです。

  • GoodレコードをPostテーブルと分ける必要があるのか?という点は今回は考慮しないものとします。
  • Djangoのプロジェクト名はsample、アプリケーション名はapp1とします。

 

models.pyの設定

モデルの設定は以下の通り、Postクラスと、Goodクラスを定義します。

python


from django.db import models



class Post(models.Model):

    

    

    class Meta:

        verbose_name ="ブログ記事"

        verbose_name_plural ="ブログ記事"

    

    title = models.CharField(verbose_name ="記事タイトル", max_length=100)

    published = models.DateTimeField(verbose_name ="公開日時")

    body = models.TextField(verbose_name ="内容")





    def __str__(self):

        return self.title





class Good(models.Model):

    """

    いいね機能

    """

    title_name = models.ForeignKey(Post, on_delete = models.CASCADE, verbose_name="記事タイトル")

    good_num = models.IntegerField("いいね", default = 0)



    class Meta:

        verbose_name = verbose_name_plural = 'いいね数'

        verbose_name_plural ="いいね数"

    

    def __str__(self):

        return str(self.title_name)

 

この辺は特に難しい点はないかと思います。

admin.pyの設定

adminページ上から各テーブルのレコードを参照、操作できるようにadmin.pyに以下の設定をしておきます。

python


from django.contrib import admin

from app1.models import Post, Good



class PostAdmin(admin.ModelAdmin):

    list_display=('pk', 'title', 'published', 'body')



class GoodAdmin(admin.ModelAdmin):

    list_display=('pk', 'title_name', 'good_num')





admin.site.register(Post, PostAdmin)

admin.site.register(Good, GoodAdmin)

 

ここまで設定したら、一旦マイグレーションとアクセス用のユーザIDを登録しておきます。

python


python manage.py makemigrations

python manage.py migrate

python manage.py createsuperuser

 

python manage.py runserverを実行してhttp://127.0.0.1:8000/adminにアクセスしてログオンします。

以下のようにブログ記事と、いいね数のテーブルが表示されていればOKです。

ブログ記事(Post)テーブルで1件、ブログ記事を登録してみます。

Postテーブルを上記のように1件登録しても、いいねテーブル側にはレコードは登録されませんので、以下のように新規登録した投稿記事に関連するいいねレコードをマニュアルで登録する必要があります。

このようなケースでは、登録したPost(記事)に紐づくいいねテーブル(Good)のレコードも自動的に登録されてほしいですよね。

そんな時にDjangoのシグナルを使うと上記のような処理を実現することができます。

以降が、具体的なシグナルの実装になります。

models.pyでPOSTクラスのsaveメソッドをオーバライドする。

新規登録されたPostテーブルのレコードのPK番号をリターンするようにsaveメソッドをオーバーライドします。

python


from django.db import models



class Post(models.Model):

    

    

    class Meta:

        verbose_name ="ブログ記事"

        verbose_name_plural ="ブログ記事"

    

    title = models.CharField(verbose_name ="記事タイトル", max_length=100)

    published = models.DateTimeField(verbose_name ="公開日時")

    body = models.TextField(verbose_name ="内容")

    

    # ここから追加

    def save(self, *args, **kwargs):

        

        super(Post, self).save(*args, **kwargs)

        context = {'post_title': self.pk,}

        return context

    # ここまでを追加





    def __str__(self):

        return self.title





class Good(models.Model):

    """

    いいね機能

    """

    title_name = models.ForeignKey(Post, on_delete = models.CASCADE, verbose_name="記事タイトル")

    good_num = models.IntegerField("いいね", default = 0)



    class Meta:

        verbose_name = verbose_name_plural = 'いいね数'

        verbose_name_plural ="いいね数"

    

    def __str__(self):

        return str(self.title_name)

 

models.Modelのsaveメソッドをオーバライドして、新規登録されたレコードのpk番号を取得(self.pk)し、context辞書変数に格納してreturnしてあげます。
モデルの定義は以上です。

urls.pyとviews.pyの設定

今回は特に画面表示までは作りこみませんが、Djangoシグナルを利用する場合、urls.pyと参照先のビュー関数を何か定義しておかないとシグナルが正常に動作しなかったため以下のようなtop関数の定義を入れておきます。
app1/urls.py
python


from django.conf.urls import url



from . import views



from django.urls import path





app_name = 'app1'

urlpatterns = [

    path(r'top/',views.top, name='top'),

]

 

sample/urls.py

python


from django.contrib import admin

from django.urls import path, include



urlpatterns = [

    path('admin/', admin.site.urls),

    path('', include('app1.urls')),

]
app1/views.py
python


from django.shortcuts import render

from django.db.models.signals import post_save

from django.dispatch import receiver

from .models import Post, Good





def top(request):

    pass



@receiver(post_save, sender=Post)

def signalmethod(sender, **kwargs):

    print("シグナルが実行されました。")

    contents = { str(key):val for key, val in kwargs.items() }

    post_obj = Post.objects.get(title =contents["instance"])

    Good.objects.create(title_name=post_obj)

 

@receiver以降がシグナルを実装している部分です。

まず、必要なメソッドを以下でインポートしています。

python


from django.db.models.signals import post_save

from django.dispatch import receiver

 

今回は、「モデルにデータが記録された後に実行されるシグナル」を実装したいので、シグナルの種類としてpost_saveをインポートします。

また、モデルの定義とシグナルを関連付けたい場合は、以下のような形式で定義すればよいことになっています。

python


@receiver(post_save, sender=<モデルクラス名>)

def signalmethod(sender, **kwargs):

 

今回は、Postクラスの新規レコードが登録されたことをシグナルとして処理を発動させたいので上記のようなコードにしています。

さらに、signalmethod関数の中で発動させたい処理を定義しています。

以下の部分です。

python


print("シグナルが実行されました。")

contents = { str(key):val for key, val in kwargs.items() }

post_obj = Post.objects.get(title =contents["instance"])

Good.objects.create(title_name=post_obj)

 

何をやっているかというと、1行目は単純にシグナルが発動したことを確認するためにprintしているだけです。

2行目では、内包表記を使ってkwargsに渡ってきた情報をcontents辞書型変数に格納しています。

kwargsにはmodels.pyのsaveメソッドでreturnしたcontextが格納されているので、kwargs.itemsでキー、値を取得できます。

※今回はpkだけですが、ほかの値も渡せばいろいろ受け取ることができます。

contents["instance"]のように指定することで、新規登録されたPostクラステーブルの記事名(title_name)を取得することができます。

今回は、登録された記事の情報が欲しいのでinstanceキーの値をcontents["instance"]として取得しています。

取得した記事名をもとにPostテーブルから該当記事のレコードオブジェクトを取得しpost_objとして格納しています。

さらに、post_objを使って、いいねテーブルに該当記事のレコードを登録しているのが以下の部分です。

python


Good.objects.create(title_name=post_obj)

 

ちょっとわかりづらいかもしれませんが、以上でPostテーブルに対して記事を新規追加すると、関連するGoodテーブルのレコードが自動生成されるようになります。

Postで新規にタイトル「記事2」を追加すると・・・

いいねテーブルにも以下のようにPostテーブルに新規追加したレコードを外部キーとしたいいねテーブルレコードが自動的に登録されます。

ちなみにviews.pyに渡されてくるcontents変数の中身をすべて確認すると以下のような情報が渡ってくるのが確認できます。

{'signal': <django.db.models.signals.ModelSignal object at 0x000002E792661DA0>, 'instance': <Post: 記事2>, 'created': True, 'update_fields': None, 'raw': False, 'using': 'default'}

 

本記事の作成に当たり以下のブログを参考にさせていただきました。

 

あるテーブルレコードが登録された場合にxxを実施したいというケースでいろいろと応用が利きそうですね。

以上、Djangoでテーブル登録完了時に追加処理を行いたい場合の事例紹介でした。

おすすめの記事