読者です 読者をやめる 読者になる 読者になる

インスタンスに変化があった時だけpre_save,post_saveを実行する

Djangoにおいて、何かしらのmodelインスタンスのsaveメソッドを呼び出した時に処理をホックする方法としてpre_save、post_saveシグナルを利用する方法があります。
例えば、次のようなTwitterInfoモデルがあったとします

# myapp/models.py
from django.db import models
from django.contrib.auth.models import User
class TwitterInfo(models.Manager):
    user = models.OneToOneField(User)
    name = models.CharField(max_length=15, blank=True, null=True)
    img = models.URLField(verify_exists=False, blank=True, null=True)

Userが入力したtwitterアカウントから自動的にtwitter画像のURLを取得して保存する場合、pre_saveを用いると簡単に実装できます*1

from django.db.models.signals import pre_save
from myapp.utils import get_twitter_pict
def update_twitter_info(sender, instance, **kwargs):
    instance.img = get_twitter_pict(instance.name)
pre_save.connect(update_twitter_info, sender=TwitterInfo)

これでnameが入力されていれば、そのアカウントの画像URLを自動的に保存することができます。
現状では、saveが呼ばれる度にget_twitter_pictが呼ばれますが、この処理はTwitterAPIを内部的に利用していると考えられるので、毎回毎回実行するのではなく、nameが変更されている時だけ呼び出すようにしたいです。
このような場合、僕は次のようにして解決しています

# myapp/models.py
from copy import copy
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import pre_save
from myapp.utils import get_twitter_pict
class TwitterInfo(models.Manager):
    user = models.OneToOneField(User)
    name = models.CharField(max_length=15, blank=True, null=True)
    img = models.URLField(verify_exists=False, blank=True, null=True)

    def __init__(self, *args, **kwargs):
        super(TwitterInfo, self).__init__(*args, **kwargs)
        self._old = copy(self) # 古い値を保持

def update_twitter_info(sender, instance, **kwargs):
    if instance._old.name != instance.name: # 古い値と比較
        instance.img = get_twitter_pict(instance.name)
pre_save.connect(update_twitter_info, sender=TwitterInfo)

TwitterInfoをコンストラクトする時に古い値をcopyして持たせ、その値と比較することでimgの更新が必要かどうかを判断します。

*1:post_saveだとTwitterInfoのsaveを2回呼ぶ必要が生じるのでこの場合はpre_saveを利用