Django・DRF でのTIME_ZONEについて

はじめて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が起動時に自身の環境を整えるために設定する。

http://e-class.center.yuge.ac.jp/jdk_docs/django1.4/topics/i18n/timezones.html

> TIME_ZONE のドキュメントで説明しているように、 Django は自分自 身のプロセスがデフォルトタイムゾーンで実行されるように環境変数を設定します。これは USE_TZ やカレントタイムゾーンの値にかからわず起こります。


実行環境のTZとは別に利用者の環境に応じたタイムゾーンを考慮する必要があるがDjangoではタイムゾーンをユーザーに選ばせる（デフォルトはTIME_ZONEが利用されるが）べきとしています。これをカレントタイムゾーンという。

```
カレントタイムゾーン はレンダリングに使われるタイムゾーンです。
カレントタイムゾーンは activate(). を使ってエンド ユーザの
実際のタイムゾーンをセットしておきましょう。
そうしなければ、デフォルト タイムゾーンが使われることになります。
```

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

https://narito.ninja/blog/detail/82/

```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.timezone`の`now`で日時を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する必要があります。