Djangoのモデルの時刻系のField(DateTimeField
など)にはauto_now
という属性があります。これをauto_now=True
とするとモデルを更新するたびにその時点での時刻を自動で設定してくれるので、更新時刻などを保持しておきたい場合に便利です。
class AutoAddTest(models.Model): data = models.CharField(max_length=255) # モデルの更新ごとに変更される updated_at = models.DateTimeField(auto_now=True) def __str__(self): return '{} at {}'.format(self.data, self.updated_at)
しかし、更新方法によってはauto_now=True
が動作しないケースがあり、ハマったのでそれを備忘録として残しておきます。
そもそもの動作確認
先程のAutoAddTest
で一応auto_now=True
の動作を確認しておきます。
In [1]: from myapp.models import AutoAddTest In [4]: for a in AutoAddTest.objects.all(): print(a) In [5]: AutoAddTest.objects.create(data='test1') Out[5]: <AutoAddTest: test1 at 2018-06-30 14:46:39.974447+00:00> In [6]: for a in AutoAddTest.objects.all(): print(a) test1 at 2018-06-30 14:46:39.974447+00:00
モデルをcreate
した時点でその時刻が期待どおり設定されています。(2018-06-30 14:46:39.974447+00:00
)
では、更新してみます。
In [7]: test1 = AutoAddTest.objects.get(data='test1') In [8]: test1 Out[8]: <AutoAddTest: test1 at 2018-06-30 14:46:39.974447+00:00> In [9]: test1.data = 'test1 updated' In [10]: test1.save() In [12]: test1 Out[12]: <AutoAddTest: test1 updated at 2018-06-30 14:48:15.676862+00:00>
14:46:39
であったupdated_at
が14:48:15
に期待通り変更されていることがわかります。一応updated_at
を明示した場合の動作も見ておきます。
In [30]: import datetime In [31]: test1 = AutoAddTest.objects.get(data='test1 updated') In [32]: test1.updated_at = datetime.datetime(1970, 1, 1) In [33]: test1.save() In [34]: test1 Out[34]: <AutoAddTest: test1 updated at 2018-06-30 14:52:11.182157+00:00>
任意の日付を明示したとしてもそれは無視され、あくまで更新した時刻が設定されます。
auto_add
のソース部分
基本的な動作はわかりましたが、もう少し深く見ておきます。auto_now
はDateTimeField
のコンストラクタの引数ですので、そのあたりのソースを確認します。厳密にはDateField
で定義されておりそちらを継承しています。
# django.db.models.fields.DateField class DateField(DateTimeCheckMixin, Field): : def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add if auto_now or auto_now_add: kwargs['editable'] = False kwargs['blank'] = True super(DateField, self).__init__(verbose_name, name, **kwargs)
実際に現在時刻が設定される部分はDateTimeField
にあります。
# django.db.models.fields.DateTimeField class DateTimeField(DateField): : : # __init__ is inherited from DateField : def pre_save(self, model_instance, add): if self.auto_now or (self.auto_now_add and add): value = timezone.now() setattr(model_instance, self.attname, value) return value else: return super(DateTimeField, self).pre_save(model_instance, add)
まぁわかりやすいですね。save
実行時の手前で呼び出されるpre_save
内でauto_now
が設定されていれば、自身の属性名(self.attname
)に対してtimezone.now()
を設定しています。なお、or
で繋がれているauto_now_add
はauto_now
と似ていますが、create
時のみ時刻を保存するものです。
つまり、auto_add
はsave
が呼び出される場合にのみ計算され代入されるのです。
auto_add
が動かないケース
ここまでの流れを踏まえると、自明ではあるのですがauto_add
が働かないのは以下のようにQuerySet
経由で更新を行うケースです。
AutoAddTest.objects.filter(data='test1').update(data='test1')
この場合、update
で指定した情報以外は変更されずauto_add
を指定した列はpre_save
が呼び出されないため更新が発生しないのです。一応結果を見ておきます。
In [40]: AutoAddTest.objects.filter(data='test1')[0].updated_at Out[40]: datetime.datetime(2018, 6, 30, 14, 52, 11, 182157, tzinfo=<UTC>) In [41]: AutoAddTest.objects.filter(data='test1').update(data='test1') Out[41]: 1 In [42]: AutoAddTest.objects.filter(data='test1')[0].updated_at Out[42]: datetime.datetime(2018, 6, 30, 14, 52, 11, 182157, tzinfo=<UTC>) # 変更前と変わっていない
これはQuerySet
のupdate
がsave
を経由せず、直接SQLを発行しているためです。
# django.db.models.query.QuerySet#update def update(self, **kwargs): """ Updates all elements in the current QuerySet, setting all the given fields to the appropriate values. """ assert self.query.can_filter(), \ "Cannot update a query once a slice has been taken." self._for_write = True query = self.query.clone(sql.UpdateQuery) query.add_update_values(kwargs) # Clear any annotations so that they won't be present in subqueries. query._annotations = None with transaction.atomic(using=self.db, savepoint=False): rows = query.get_compiler(self.db).execute_sql(CURSOR) self._result_cache = None return rows
いろいろやっていますがrows = query.get_compiler(self.db).execute_sql(CURSOR)
で直接組み立てたUPDATE文を発行しているため、auto_add
の値の計算などは考慮されないのです。ここはできれば対応してほしいような気もするのですが、、関連するっぽいBUGがありました。
Document auto_now behavior with QuerySet.update()
しかし、このBugはこれまで述べてきた動作がドキュメントになかったことを問題として扱い、ドキュメントに注意書きを追加することでCloseしていますので動作が変更されることは、恐らくなさそうです(少なくともしばらくは?)
一応今回のケースであれば以下のように、updated_atをちゃんと明示することで対応できます。
In [43]: from django.utils import timezone In [44]: AutoAddTest.objects.filter(data='test1').update(data='test1', updated_at=timezone.now()) Out[44]: 1