Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

世間はサマータイム導入の議論で盛り上がってますが、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の補正を追加

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

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

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に影響を受けるというところが大きな違いだと思います。

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

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

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

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

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

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

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

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

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

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の補正を追加

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

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

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を意識せず書けるかといったらそうではありません。単なるフィールドの読み書きに過ぎません。

// 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型に変更するのがいいように思います。

sandabu
Web(RTC)やVRなどに興味があります
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away