こんにちは!内藤です!
ようやく札幌も気温と湿度が下がって過ごしやすくなってきました。
あっという間に冬が来るのだろうなぁ、と、いまからドキドキしています。
先日、DjangoのDateTimeFieldを持つModelを表示・編集する画面を作成していて、タイムゾーン問題に当たりました。
発生した問題自体は、画面上の時間が9時間早い日時になるというタイムゾーンがUTCになっているのが原因のありがちな問題です。
しかしながら、Djangoのタイムゾーン設定がどのように作用するのかイマイチわかっていなかったので、これを機に実験してみました。
やってみたこと
Djangoの USE_TZ, TIME_ZONEとデータベース(PostgreSql)のタイムゾーン設定を変更する
それぞれの場合で、
- Model生成直後のDateTimeFieldのtzinfoと、strftimeの結果
- データベースから読みだした直後のDateTimeFieldのtzinfoと、strftimeの結果がどうなるかを調査
TL;DR
- データベース側のタイムゾーン情報は関係ない
- Django側の USE_TZ の値によってDateTimeFieldの値が変わる
- USE_TZ=Trueなら、内部的には常にtzinfo=UTCのAwareなDateTimeになる
- USE_TZ=Falseなら、内部的には常にNaiveなDateTimeになる
- USE_TZ=Trueの時、django.utils.localtime関数をかませると、TIME_ZONEで設定したタイムゾーンのAwareなDateTimeになる
前提知識
- そもそもdatetimeのAwareとNaiveとは?
- pythonの公式の説明がすべてです
- めっちゃ単純化すると、タイムゾーン情報(tzinfoフィールド)が設定されているかどうかです
実験内容
実験コード
下記のようなDjangoのカスタムコマンドを作成し、それぞれの環境で実行します。
Docker環境も合わせた実験環境は、GitHubのリポジトリを参照してください。
from django.core.management.base import BaseCommand
from apps.main.models import SampleTimestamp
class Command(BaseCommand):
help = "チェック: DateTimeFieldのtzinfoとstrftimeの挙動"
def handle(self, *args, **options):
self.stdout.write("\n=== 新規作成時 ===")
obj = SampleTimestamp.objects.create(name="test")
self._dump_datetime("生成直後", obj.created_at)
self.stdout.write("\n=== DBから再取得後 ===")
obj = SampleTimestamp.objects.get(id=obj.id)
self._dump_datetime("DB取得後", obj.created_at)
def _dump_datetime(self, label, dt):
from django.utils.timezone import localtime
self.stdout.write(f"{label}: {dt} (tzinfo={dt.tzinfo})")
self.stdout.write(f" strftime: {dt.strftime('%Y-%m-%d %H:%M:%S')}")
try:
local = localtime(dt)
self.stdout.write(f" localtime + strftime: {local.strftime('%Y-%m-%d %H:%M:%S %Z')}")
except Exception as e:
self.stdout.write(f" localtime failed: {e}")
実験結果
実験結果として、それぞれの場合のtzinfoをまとめます。
PostgreSql側の設定は結果に影響がなかったので、パターンとして省きます。
DjangoのUSE_TZ=True, TIME_ZONE=Asia/Tokyoの場合
生成直後 | 生成直後のlocaltimeの結果 | DB読み出し直後 | DB読み出し直後localtimeの結果 |
---|---|---|---|
UTC | Asia/Tokyo | UTC | Asia/Tokyo |
DjangoのUSE_TZ=True, TIME_ZONE=UTCの場合
生成直後 | 生成直後のlocaltimeの結果 | DB読み出し直後 | DB読み出し直後localtimeの結果 |
---|---|---|---|
UTC | UTC | UTC | UTC |
DjangoのUSE_TZ=Falseの場合
生成直後 | 生成直後のlocaltimeの結果 | DB読み出し直後 | DB読み出し直後localtimeの結果 |
---|---|---|---|
None | 例外発生 | None | 例外発生 |
まとめ
DjangoのDateTimeFieldの値は、USE_TZ=Trueを設定しても、TIME_ZONEで設定したタイムゾーンではなくUTCが設定されています。
strftime等使用してシリアライズする時にTIME_ZONEで設定したタイムゾーンを使用したい場合、django.utils.timezone.localtimeを使用する必要があります。
(これを忘れると、画面側の日時だけ9時間早くなります・・・)
終わりに
公式ドキュメントに書いてある通りといえば、書いてある通りの挙動です。
実際に試してみて、ちょっとすっきりしました。
参照URL
DRF + MySQLでの動作について検証している方がいらっしゃいました。