Django モデルに登録日時や更新日時のフィールドを付加する場合、「auto_now_add」オプションや「auto_now」オプションを利用すると便利です。それらのオプションの挙動について詳しく調べてみたので、まとめておきます。
TL;DR
フィールドの auto_now_add, auto_now オプションについて
— akiyoko/ 『現場で使えるDjangoの教科書』 (@aki_yok) May 21, 2020
登録時
・auto_now_add=True
・auto_now=True
更新時
・auto_now=True
のフィールドに現在日時が自動でセットされる。
ただし、任意の値をセットできない。https://t.co/jqOYzylLxj
freezegun を使うと簡単にテストできる。#Djangoメモ
挙動
たとえば、
shop/models.py
from django.db import models class Book(models.Model): """本モデル""" class Meta: db_table = 'book' verbose_name = verbose_name_plural = '本' title = models.CharField('タイトル', max_length=255) price = models.PositiveIntegerField('価格', null=True, blank=True) created_at = models.DateTimeField('登録日時', auto_now_add=True) updated_at = models.DateTimeField('更新日時', auto_now=True) def __str__(self): return self.title
というフィールドがあるとすると、
登録時
- 「auto_now_add=True」のフィールド(created_at)に現在日時が自動的にセットされる
- 「auto_now=True」のフィールド(updated_at)に現在日時が自動的にセットされる
更新時
- 「auto_now=True」のフィールド(updated_at)に現在日時が自動的にセットされる
という挙動になります。
つまり、テストを書くとこういうことになります(参考までに freezegun と unittest.mock を使ったものを併記)。
shop/tests/test_models.py
from datetime import datetime from unittest.mock import patch from django.test import TestCase from django.utils import timezone from freezegun import freeze_time from ..models import Book class TestBook(TestCase): @freeze_time('2020-01-01 01:01:01') def test_save_with_freezegun(self): book = Book(title='Django本', price=1500) # 登録 book.save() # 登録日時・更新日時のどちらも値がセットされる self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) self.assertEqual(book.updated_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) with(freeze_time('2020-02-02 02:02:02')): # 更新 book.save() # 更新日時のみが更新される self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) self.assertEqual(book.updated_at, datetime(2020, 2, 2, 2, 2, 2, tzinfo=timezone.utc)) @patch('django.utils.timezone.now', return_value=datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) def test_save_with_mock(self, _mock_now): book = Book(title='Django本', price=1500) # 登録 book.save() # 登録日時・更新日時のどちらも値がセットされる self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) self.assertEqual(book.updated_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) _mock_now.return_value = datetime(2020, 2, 2, 2, 2, 2, tzinfo=timezone.utc) # 更新 book.save() # 更新日時のみが更新される self.assertEqual(book.created_at, datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc)) self.assertEqual(book.updated_at, datetime(2020, 2, 2, 2, 2, 2, tzinfo=timezone.utc))
フィールドに現在日時が自動的にセットされる仕組みを見てみると、次のように、pre_save() で django.utils.timezone.now() が呼ばれています。
django.db.models.fields.DateTimeField.pre_save *1
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().pre_save(model_instance, add)
そして django.utils.timezone.now() では、Django の設定ファイルの「USE_TZ」が True の場合は datetime.datetime.utcnow() が呼ばれています。
django.utils.timezone.now *2
def now(): """ Return an aware or naive datetime.datetime, depending on settings.USE_TZ. """ if settings.USE_TZ: # timeit shows that datetime.now(tz=utc) is 24% slower return datetime.utcnow().replace(tzinfo=utc) else: return datetime.now()
freezegun の freezegun.api.FakeDatetime が次のように datetime.datetime.now() や utcnow() などをモックしてくれているので、「auto_now_add」や「auto_now」にも適用できるのですね。よかった。
freezegun.api.FakeDatetime *3
class FakeDatetime(with_metaclass(FakeDatetimeMeta, real_datetime, FakeDate)): ...(略)... @classmethod def now(cls, tz=None): now = cls._time_to_freeze() or real_datetime.now() if tz: result = tz.fromutc(now.replace(tzinfo=tz)) + cls._tz_offset() else: result = now + cls._tz_offset() return datetime_to_fakedatetime(result) ...(略)... @classmethod def utcnow(cls): result = cls._time_to_freeze() or real_datetime.utcnow() return datetime_to_fakedatetime(result) ...(略)...
「auto_now_add」や「auto_now」を使うことによる弊害
便利でよいのですが、二点ほど弊害があります。
1.任意の値で更新できない
実例を挙げると、
>>> from shop.models import Book >>> from datetime import datetime >>> from django.utils import timezone >>> book = Book() >>> book.updated_at = datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc) >>> book.save() >>> book.updated_at datetime.datetime(2020, 5, 21, 6, 41, 7, 219328, tzinfo=<UTC>)
という感じで、保存時に現在日時で上書きされてしまいます。
(参考)www.ianlewis.org
しかしながら、登録日時や更新日時フィールドに「auto_now_add」および「auto_now」オプションを利用する場合、任意の値で更新したいユースケースとしてはユニットテストのときくらいでしょうから、実害はあまり無いと言えるかもしれません。
2.管理サイトの詳細画面で表示されない
「auto_now_add」もしくは「auto_now」オプションを利用すると、自動的に editable オプションが False になるので、管理サイトの詳細画面で表示されなくなります。
django.db.models.fields.DateField.__init__ *4
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().__init__(verbose_name, name, **kwargs) ...(略)... class DateTimeField(DateField): ...(略)... # __init__ is inherited from DateField
詳細画面にフィールドを表示するだけであれば(任意の値で登録・更新できないが)、ModelAdmin の「fields」と「readonly_fields」に同時に列挙することで対応は可能です。
shop/admin.py
from django.contrib import admin from .models import Book class BookAdmin(admin.ModelAdmin): fields = ('title', 'price', 'created_at', 'updated_at') readonly_fields = ('created_at', 'updated_at') admin.site.register(Book, BookAdmin)
これらの弊害については一応解決策がありますが、ちょっと面倒です。あまりオススメしません。
宣伝
Django の本を3冊書きました。Django 開発のお供にどうぞ。
現場で使える Django の教科書《基礎編》
★ Amazon(電子版/ペーパー版)
★ BOOTH(ペーパー版)
現場で使える Django の教科書《実践編》
★ Amazon(電子版)
★ BOOTH(ペーパー版)
現場で使える Django REST Framework の教科書
★ Amazon(電子版)
★ BOOTH(ペーパー版)
*1:https://github.com/django/django/blob/2.2.12/django/db/models/fields/__init__.py#L1402-L1408
*2:https://github.com/django/django/blob/2.2.12/django/utils/timezone.py#L224-L232
*3:https://github.com/spulec/freezegun/blob/0.3.15/freezegun/api.py#L378-L381
*4:https://github.com/django/django/blob/2.2.12/django/db/models/fields/__init__.py#L1160-L1166