LaravelをJST、MySQLをUTCで運用する


世間はサマータイム導入の議論で盛り上がってますが、Timezoneとかってつらいですよね。

プロジェクトが国内サービスと確定しているならDBもApp側(Laravel)もTimezoneをJSTとするのがわかりやすいです。

しかし、国際対応など考えるとDBにはUTCで格納してAppはJSTとして扱うのがスケールするときにも良いですし、なにより精神衛生的にスッキリするかと思います。

主にEloquentによってAppとDBを繋いでいるのですが、ここらへんのTimezoneの違いをどのように吸収すればいいのか調べて実装してみました。

# 解決法(timestamp型カラム)

```config/app.php
    'timezone' => 'Asia/Tokyo',
```

LaravelのtimezoneをAsia/Tokyoに変更

```config/database.php
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
            //MySQLのTZはUTC、LaravelのTZはJSTのため、+09:00の補正を入れる
            //ReadはJST、WriteはUTC
            'timezone' => '+09:00',
        ],
```

timezoneの+09:00の補正を追加


```php:Migration
Schema::table('events', function(Blueprint $table) {
    $table->timestamp('closed_at')->nullable();
});
```

例えばこのようなテーブル・カラムがあったとき

```php:event.php
protected $dates = [
    'closed_at',
];
```

そのモデルの$datesに追加すれば、Eloquentを用いる時に読み取りはMySQLのUTCからJSTに変換され、書き込みはJSTからUTCに変換されます。

# timestamp型ではなくdatetime型のカラムの場合

先程の方法は、timestamp型ならできる方法でした。(datetime型の場合、DBに書き込み時、JSTになってしまう)

datetime型はどうすりゃええんや...となった場合です。

## TL;DR

まだ稼働していない-> timestamp型に強制変更。datetimeをDrop & timestampをAdd(データは消える)

もう稼働している-> アクセサ・ミューテータでUTC<=>JST変換

## timestamp vs datetime

そもそもtimestamp型とdatetime型って何が違うんでしょうか。

### timestamp型
- 1970-01-01 00:00:00から2038-01-19 03:14:07
- timezoneに影響を受ける
- 2038年問題がある

### datetime型
- 1000-01-01 00:00:00から9999-12-31 23:59:59
- timezoneに影響を受けない
- 日時の一点を表す

他にも違いはあるんですが、ざっくりだと以上のような違いがあります。

個人的には、timestamp型がTimezoneに影響を受けるというところが大きな違いだと思います。

ちなみに、マイグレーションにおける

```php:Migration
Schema::create('events', function (Blueprint $table) {
    $table->timestamps();
});
```

timestampsは`created_at`と`updated_at`を作成されますが、その名の通り両方ともtimestamp型です。


## datetime型からtimestamp型へはchangeできない

Doctrineの仕様で、Laravelのmigrationではできないようです。

https://qiita.com/acro5piano/items/ee03832588699c0bee14

## データが消えてしまう方法

```php:drop
Schema::table('events', function(Blueprint $table) {
    $table->dropColumn('closed_at');
});
```

drop用とは別のmigrationファイルを作成

```php:add
Schema::table('events', function(Blueprint $table) {
    $table->timestamp('closed_at')->nullable();
});
```

あとは上記の解決方法を実行します。

## アクセサ・ミューテータで変換する方法

```config/app.php
    'timezone' => 'Asia/Tokyo',
```

LaravelのtimezoneをAsia/Tokyoに変更

```config/database.php
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
            //MySQLのTZはUTC、LaravelのTZはJSTのため、+09:00の補正を入れる
            //ReadはJST、WriteはUTC
            'timezone' => '+09:00',
        ],
```

timezoneの+09:00の補正を追加

```php:Migration
Schema::table('events', function(Blueprint $table) {
    $table->timestamp('closed_at')->nullable();
});
```

このようなテーブルとカラムがあったとき

```php:event.php
//アクセサ
public function getClosedAtAttribute($value) {
    //アプリケーションで使うtzはJST
    $datetime = new Carbon($value, 'UTC');
    $datetime->setTimezone('JST');

    return $datetime;
}

//ミューテータ
public function setClosedAtAttribute($value) {
    //DBに保存するtzはUTC
    $datetime = new Carbon($value, 'JST');
    $datetime->setTimezone('UTC');
    $this->attributes['closed_at'] = $datetime;
}
```

該当のモデルに、それぞれアクセサとミューテータを実装して解決できます。

### 注意

アクセサとミューテータによってTimezoneを意識せず書けるかといったらそうではありません。単なるフィールドの読み書きに過ぎません。

```php:
// Read
dump($event->closed_at);
// -> アクセサによってCarbon(JST)が読み出される

// Write
$event->closed_at = Carbon::now('JST');
$event->save();
// -> ミューテータによってUTCとして保存される
```

このような処理以外にも、QueryBuilderやEloquentのwhereはUTCで扱われます。

```php:
Event::where('closed_at', '>', Carbon::now('UTC'))->get();
```

whereで条件引っ掛ける場合はUTCでないと正しいクエリが発行されないので注意です。

開発中にJSTとかUTCとか意識するのは嫌ですよね。やはりtimestamp型に変更するのがいいように思います。