はじめてDjango, DRFを使った時に TimeStampedModel
がする事や設定がわからずタイムゾーンについて調べたので、メモとして残しておきます。
まずは結論 この設定にしておけばOK
# Local time zone. Choices are
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS.
# In Windows, this must be set to your system time zone.
TIME_ZONE = 'Asia/Tokyo'
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = 'ja'
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
USE_L10N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
Djangoは環境変数TZやTIME_ZONE設定をどう使うのか
Djangoは環境変数TZやタイムゾーンを知るためのヒントとなり得る accept_language
を利用しない。環境変数TZはDjangoが起動時に自身の環境を整えるために設定する。
TIME_ZONE のドキュメントで説明しているように、 Django は自分自 身のプロセスがデフォルトタイムゾーンで実行されるように環境変数を設定します。これは USE_TZ やカレントタイムゾーンの値にかからわず起こります。
実行環境のTZとは別に利用者の環境に応じたタイムゾーンを考慮する必要があるがDjangoではタイムゾーンをユーザーに選ばせる(デフォルトはTIME_ZONEが利用されるが)べきとしています。これをカレントタイムゾーンという。
カレントタイムゾーン はレンダリングに使われるタイムゾーンです。
カレントタイムゾーンは activate(). を使ってエンド ユーザの
実際のタイムゾーンをセットしておきましょう。
そうしなければ、デフォルト タイムゾーンが使われることになります。
その場合もサンプルも提示されている。
from django.utils import timezone
import pytz
def timezone_middleware(get_response):
def middleware(request):
tzname = request.session.get('django_timezone')
if tzname:
timezone.activate(pytz.timezone(tzname))
else:
timezone.deactivate()
response = get_response(request)
return response
return middleware
このactivateはなんとなく記載的にグローバル汚染をしているように見えるが、実際はリクエストを処理するスレッドにタイムゾーンを割り当てる処理。
timezone.activate(pytz.timezone(tzname))は、その取得したタイムゾーンを
Djangoに実際に適用する処理です。
イメージとしては、settings.TIME_ZONEの値に代入していると思ってください
(実際にはスレッド毎にタイムゾーンを持たせる必要があるので、もう少し違う形です)。
DjangoのDateTimeFieldについて
このフィールドはMySQLでは DateTime型
にマッピングされている。MySQLのDateTime型は、タイムゾーンを持たない。INSERT時もSELECT時も何も変更(表現の変更)をしない。
ちなみにMySQLでタイムゾーンを意識したい場合は Timestamp型
を使うが、Djangoにこの型に対応したフィールドはない。
そしてUSE_TZ=Trueの場合は、django.utils.timezone
のnow
で日時をDBに入れるとUTCになります。 TimeStampedModel
もこのnowを利用しています。
ここまでにsetting.TIME_ZONEは全く登場しません。USE_TZがTrueならUTCで、そうでなければ現在の環境に応じた時刻(僕らの環境ならJST)をDBに入れるだけです。
DRFがUTCをどうやって地域時間に変更しているか
def enforce_timezone(self, value):
"""
When `self.default_timezone` is `None`, always return naive datetimes.
When `self.default_timezone` is not `None`, always return aware datetimes.
"""
field_timezone = getattr(self, 'timezone', self.default_timezone())
if field_timezone is not None:
if timezone.is_aware(value):
try:
return value.astimezone(field_timezone)
except OverflowError:
self.fail('overflow')
try:
return timezone.make_aware(value, field_timezone)
except InvalidTimeError:
self.fail('make_aware', timezone=field_timezone)
elif (field_timezone is None) and timezone.is_aware(value):
return timezone.make_naive(value, utc)
return value
呼び出しスタック的にはここから
return Response(serializer.data)
ここから辺で初めてTIME_ZONEが出てくる。表示するタイムゾーンに従って
localTimeを計算するから出てくるので、ごく自然。
@functools.lru_cache()
def get_default_timezone():
"""
Return the default time zone as a tzinfo instance.
This is the time zone defined by settings.TIME_ZONE.
"""
return pytz.timezone(settings.TIME_ZONE)
誤解のあるMySQLのタイムゾーン
タイムゾーン関係の設定はこんな感じです。
SHOW VARIABLES LIKE '%time_zone%';
system_time_zone UTC
time_zone Asia/Tokyo
これらの設定をみてMySQLが空気を読んで時刻を調整してくれるは Timestamp型
のフィールド、またはNOW()のような関数です。
Datetime型はまったく影響を受けません。
https://qiita.com/ryokkkke/items/a007d5edd4d8e7484c56
挙動としてはこんな感じ
datetime
-
INSERT時
与えられた日付時刻リテラルを、time_zoneに関係なくそのまま保存する。 -
SELECT時
保存した日付時刻リテラルをそのまま渡す。
timestamp
-
INSERT時
与えられた日付時刻リテラルを、time_zoneで指定されたタイムゾーンでの時刻であると解釈し、それをUTC時刻に変更して保存する。 -
SELECT時
保存したUTC時刻をtime_zoneで指定されたタイムゾーンでの表記に変更して渡す。
USE-TZ=Falseはやめましょう
USE_TZ=Falseにすれば、JSTでDBに値が放り込まれるので、色々簡単(ただしMySQLのTimezoneは設定する必要がある)。しかし今後マルチロケール化するときに地獄を見ることになる。サマータイムでも地獄を見ることになるでしょう。
USE_TZ=Trueで運用すべきですが、True時には何も意識しなければDjangoのSerializerがよきに処理してくれますが、DB上にUTCで入ることで混乱しがちです。またMySQLに直接SQLを投げるときにもConvert_tzする必要があります。