akiyoko blog

akiyoko の IT技術系ブログです

Django モデルのフィールドの「auto_now_add」「auto_now」オプションの挙動を詳しく調べてみた

Django モデルに登録日時や更新日時のフィールドを付加する場合、「auto_now_add」オプションや「auto_now」オプションを利用すると便利です。それらのオプションの挙動について詳しく調べてみたので、まとめておきます。

f:id:akiyoko:20200521233958p:plain

TL;DR


挙動

たとえば、

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)に現在日時が自動的にセットされる

という挙動になります。


つまり、テストを書くとこういうことになります(参考までに freezegununittest.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 になるので、管理サイトの詳細画面で表示されなくなります。

f:id:akiyoko:20200521232712p:plain

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)

f:id:akiyoko:20200521233140p:plain



これらの弊害については一応解決策がありますが、ちょっと面倒です。あまりオススメしません。

stackoverflow.com



 

宣伝

Django の本を3冊書きました。Django 開発のお供にどうぞ。

現場で使える Django の教科書《基礎編》

★ Amazon(電子版/ペーパー版)


★ BOOTH(ペーパー版)


現場で使える Django の教科書《実践編》

★ Amazon(電子版)


★ BOOTH(ペーパー版)


現場で使える Django REST Framework の教科書

★ Amazon(電子版)


★ BOOTH(ペーパー版)