LoginSignup
8

More than 3 years have passed since last update.

Django・DRF でのTIME_ZONEについて

Posted at

はじめてDjango, DRFを使った時に TimeStampedModel がする事や設定がわからずタイムゾーンについて調べたので、メモとして残しておきます。

まずは結論 この設定にしておけばOK

setting.py
# 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(). を使ってエンド ユーザの
実際のタイムゾーンをセットしておきましょう。
そうしなければ、デフォルト タイムゾーンが使われることになります。

その場合もサンプルも提示されている。

middleware.py
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.timezonenowで日時をDBに入れるとUTCになります。 TimeStampedModel もこのnowを利用しています。

ここまでにsetting.TIME_ZONEは全く登場しません。USE_TZがTrueならUTCで、そうでなければ現在の環境に応じた時刻(僕らの環境ならJST)をDBに入れるだけです。

DRFがUTCをどうやって地域時間に変更しているか

rest_framework/fields.py
    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する必要があります。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8