diadia

興味があることをやってみる。自分のメモを残しておきます。

Django シグナル POST_SAVEのupdate_fieldsをどう使うか

シグナルpost_saveのupdate_fieldsの用途

例えばこんな時に使いたい時にupdate_fieldsが役に立つ。

class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
    adm0 = models.CharField(max_length=15, default="Japan", choices=adm0_CHOICES)
    adm1  = models.CharField(max_length=15, null=False, choices=DEPARTAMENTO_CHOICES)
    adm2 = models.CharField(max_length=30, null=False, choices=MUNICIPIO_CHOICES)
    description  = models.TextField(default=DEFAULT_PROFILE_DESCRIPTION)
    point = geomodels.PointField(null=True, blank=True)

Ptofileオブジェクトには国の属性を示すadm0、都道府県の属性を示すadm1、市を示すadm2が存在する。一方でユーザーの座標情報を格納するpoint(ジオメトリ型データ)が存在する。

ユーザーのProfileオブジェクトはユーザーの好みによって2種類の生成もしくは変更方法がある。

  • ユーザーが都道府県、市を各リストから選択して登録する方法
  • ユーザーが位置情報を入力し、webアプリが位置情報に合致した都道府県、市を探し登録する方法

後者のProfileオブジェクトのpointが変更されたときだけ、シグナルが発行され、座標データに従って都道府県、市を変更するを実現する場合には、post_saveのcreatedだけではうまく作動させることができない。しかしpost_saveシグナルが発火し、かつpointが変更されたという状況にのみ都道府県、市を変更するコードが動くとしたらやりたいことを実現できる。そしてこれはupdate_fieldsを使うなら実現できる。

イメージサンプルコード

def hoge(sender, instance, created, update_fields, *args, **kwargs):
    if created == True:
        return 
    elif created == False and "point" in  update_fields:
        #point属性値に従ってadm1, adm2を変更するロジックを書く
        return 

post_save.connect(hoge, sender=Profile)

上記のようにコードを記述するだけではシグナルがうまく作動しない。
上記のコードには誤りはないのだけれども、これだけでは足りないのである。

Noneの値が常に出てしまう問題

イメージサンプルコードを走らせるとupdate_fieldsの値がことごとくNoneとなってしまうのである。 したがって期待するロジックが実行されない問題に直面することになる。これはupdate_fieldsの使い方に問題があるからだ。

じゃあupdate_fieldsをどう使うべきなのか?

update_fields(シーケンス型のデータ)はコード記述者が自ら要素を格納しないといけない。 具体的にはあるクラスオブジェクトのsave()メソッド実行時にupdate_fieldsを使うのである。

profile_obj = Profile.objects.get(id=2)
profile_obj.point = point 
profile_pbj.save(update_fields=["point"])

上記のようにオブジェクトの更新に際してupdate_fields=["point"]をsave()メソッドの引数とするのである。
こうすると晴れてシグナルのupdate_fieldsの値はNoneではなく、"point"に関連したデータになるのである。

しかし問題点も存在する。上記の方法ではviews.pyのロジック中に記述しているのだけれども、例えばモバイルアプリケーションを通じてオブジェクトの変更する場合にはロジック中に記述する事ができないのである。 それはモバイルアプリを通じてデータを受信する場合にDRFを使うとSerializerを使ことになるからである。この場合にはserializer.save()を使うのだけれどもこのsave()はupdate_fields=["point"]を許容することができないのだ。 この場合には例えば以下のようにして対応する。

serializer = ProfileSerializer(profile_obj, data=request.data)
if serializer.is_valid():
    if "point" in request.data.keys():
        #シグナルを実行するためにmodel.save()を実行する
        serializer.save()
        serializer.instance.save(update_fields=["point"])
    else:
        serializer.save()