世間はサマータイム導入の議論で盛り上がってますが、Timezoneとかってつらいですよね。
プロジェクトが国内サービスと確定しているならDBもApp側(Laravel)もTimezoneをJSTとするのがわかりやすいです。
しかし、国際対応など考えるとDBにはUTCで格納してAppはJSTとして扱うのがスケールするときにも良いですし、なにより精神衛生的にスッキリするかと思います。
主にEloquentによってAppとDBを繋いでいるのですが、ここらへんのTimezoneの違いをどのように吸収すればいいのか調べて実装してみました。
解決法(timestamp型カラム)
'timezone' => 'Asia/Tokyo',
LaravelのtimezoneをAsia/Tokyoに変更
'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の補正を追加
Schema::table('events', function(Blueprint $table) {
$table->timestamp('closed_at')->nullable();
});
例えばこのようなテーブル・カラムがあったとき
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に影響を受けるというところが大きな違いだと思います。
ちなみに、マイグレーションにおける
Schema::create('events', function (Blueprint $table) {
$table->timestamps();
});
timestampsはcreated_at
とupdated_at
を作成されますが、その名の通り両方ともtimestamp型です。
datetime型からtimestamp型へはchangeできない
Doctrineの仕様で、Laravelのmigrationではできないようです。
データが消えてしまう方法
Schema::table('events', function(Blueprint $table) {
$table->dropColumn('closed_at');
});
drop用とは別のmigrationファイルを作成
Schema::table('events', function(Blueprint $table) {
$table->timestamp('closed_at')->nullable();
});
あとは上記の解決方法を実行します。
アクセサ・ミューテータで変換する方法
'timezone' => 'Asia/Tokyo',
LaravelのtimezoneをAsia/Tokyoに変更
'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の補正を追加
Schema::table('events', function(Blueprint $table) {
$table->timestamp('closed_at')->nullable();
});
このようなテーブルとカラムがあったとき
//アクセサ
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を意識せず書けるかといったらそうではありません。単なるフィールドの読み書きに過ぎません。
// Read
dump($event->closed_at);
// -> アクセサによってCarbon(JST)が読み出される
// Write
$event->closed_at = Carbon::now('JST');
$event->save();
// -> ミューテータによってUTCとして保存される
このような処理以外にも、QueryBuilderやEloquentのwhereはUTCで扱われます。
Event::where('closed_at', '>', Carbon::now('UTC'))->get();
whereで条件引っ掛ける場合はUTCでないと正しいクエリが発行されないので注意です。
開発中にJSTとかUTCとか意識するのは嫌ですよね。やはりtimestamp型に変更するのがいいように思います。