Laravelでミリ秒精度の日時を扱うときのTips
Laravelで開発中に、
「ミリ秒で保存できないーーー!!!!」
「なんか全部2022-10-01 12:34:56.000
みたいに小数点以下が0になるーー!!」
ってイライラしたのでまとめてみました。
ℹ️ 要約
Eloquentモデルの$casts
に'datetime:Y-m-d H:i:s.v'
を指定するだけだと小数点以下の時間は保存されない。
保存前の日時指定の際に、保存したい書式でフォーマットする必要がある。
- マイグレーションファイルで引数の
$precision
に小数点以下の精度を指定する。 -
$dates
と$dateFormat
をモデルに指定する。
もしくは、
DateTime型(Carbonインスタンスなど)を保存する際にはフォーマットした値を指定する。
※Laravel v9.34.0 (PHP v8.1.11)・MySQL, PostgreSQLで検証
🌀 ミリ秒まで保存したかったけどうまくいかなかった
次の章で説明する、
- マイグレーションファイルで引数の
$precision
に小数点以下の精度を指定する。
の部分はやっていたのですが、上手く行きませんでした。
その時の状況はこんな感じです。
Userモデルを作成し、日付型のカラムは$castsを指定して自動でCarbonインスタンスに変換してもらいます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasFactory;
protected $fillable = [
'name',
'email',
'password',
'first_logged_in_at'
];
protected $hidden = [
'password',
];
protected $casts = [
'first_logged_in_at' => 'datetime:Y-m-d H:i:s.v',
'created_at' => 'datetime:Y-m-d H:i:s.v',
'updated_at' => 'datetime:Y-m-d H:i:s.v',
];
}
マイグレーションファイル(後で説明あり)
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
// precisionを指定すると、保存する時間の精度を指定できる
$table->timestamps(3);
$table->dateTime('first_logged_in_at', 3);
});
}
public function down()
{
Schema::dropIfExists('users');
}
};
簡単にUserモデルを作成できるようにUserFactoryモデルを作成しました。
<?php
namespace Database\Factories;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
public function definition(): array {
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => 'password',
'first_logged_in_at' => Carbon::now() // <- Carbonインスタンスを作成
];
}
}
今回注目するべきは、UserFactoryのCarbon::now()
の部分です。
Laravelはデフォルトで、Carbonインスタンスを指定したら自動で日付文字列に変換してデータベースに保存してくれます。
Userモデルで$casts = ['first_logged_in_at' => 'datetime:Y-m-d H:i:s.v'];
を指定しているので、2022-10-01 12:34:56.789
のように年-月-日 時間:分:秒.ミリ秒
の形式でフォーマットしてくれるはずだと思っていました。
tinkerで試してみます。
% docker compose exec laravel.test php artisan tinker
Psy Shell v0.11.8 (PHP 8.1.11 — cli) by Justin Hileman
>>> $user = User::factory()->make();
=> App\Models\User {#4668
name: "Gus Jacobs",
email: "tierra92@example.net",
#password: "password",
first_logged_in_at: Carbon\Carbon @1664975793 {#3728
date: 2022-10-05 13:16:33.767047 UTC (+00:00),
},
}
>>> $user->save();
=> true
>>> $user
=> App\Models\User {#4668
name: "Gus Jacobs",
email: "tierra92@example.net",
#password: "password",
first_logged_in_at: Carbon\Carbon @1664975793 {#3728
date: 2022-10-05 13:16:33.767047 UTC (+00:00),
},
updated_at: "2022-10-05 13:16:49",
created_at: "2022-10-05 13:16:49",
id: 1,
}
>>> $savedUser = User::find(1);
=> App\Models\User {#3727
id: 1,
name: "Gus Jacobs",
email: "tierra92@example.net",
#password: "password",
created_at: "2022-10-05 13:16:49.000",
updated_at: "2022-10-05 13:16:49.000",
first_logged_in_at: "2022-10-05 13:16:33.000",
}
>>> $user->first_logged_in_at;
=> Illuminate\Support\Carbon @1664975793 {#3689
date: 2022-10-05 13:16:33.767047 UTC (+00:00),
}
>>> $user->first_logged_in_at->format('Y-m-d H:i:s.v');
=> "2022-10-05 13:16:33.767"
>>> $savedUser->first_logged_in_at->format('Y-m-d H:i:s.v');
=> "2022-10-05 13:16:33.000"
なんと、保存前($user->first_logged_in_at
)はミリ秒の値があったのに、保存後($savedUser->first_logged_in_at
)はミリ秒のデータがなくなってしまっています!!
💡対処法
以下を順番にやっていくと小数点以下の精度をDBに保存して扱えます。
- マイグレーションファイルで引数の
$precision
に小数点以下の精度を指定する。 -
$dates
と$dateFormat
をモデルに指定する。
もしくは、
DateTime型(Carbonインスタンスなど)を保存する際にはフォーマットした値を指定する。
$precision
に小数点以下の精度を指定する
1️⃣ マイグレーションファイルで以下は今回のマイグレーションファイルです。timestamps
とdateTime
の引数の$precision
に小数点以下の桁数を指定します。今回はミリ秒なので3
を指定しています。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
// precisionを指定すると、保存する時間の精度を指定できる
$table->timestamps(3);
$table->dateTime('first_logged_in_at', 3);
});
}
public function down()
{
Schema::dropIfExists('users');
}
};
migrationの実行後にテーブルスキーマを見てみるとこんな感じになっています。
ちゃんとtimestamp(3)
, datetime(3)
となっています。
mysql> describe users;
+--------------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| password | varchar(255) | NO | | NULL | |
| created_at | timestamp(3) | YES | | NULL | |
| updated_at | timestamp(3) | YES | | NULL | |
| first_logged_in_at | datetime(3) | NO | | NULL | |
+--------------------+-----------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
もし$precision
を指定しない場合はこんなふうになっています。
mysql> describe users;
+--------------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| password | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
| first_logged_in_at | datetime | NO | | NULL | |
+--------------------+-----------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
2️⃣ 保存時はFormatした値を指定する
対処法1:日時の指定時に日付文字列にフォーマット
対処する1つの方法は、日時の指定時に日付文字列にフォーマットすることです。
UserFactoryを以下のように変更することでうまくいきます。
public function definition(): array {
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => 'password',
- 'first_logged_in_at' => Carbon::now() // <- Carbonインスタンスを作成
+ 'first_logged_in_at' => Carbon::now()->format('Y-m-d H:i:s.v') // <- 日付文字列にフォーマットする
];
}
tinkerで試してみます。
>>> $user = User::factory()->make();
=> App\Models\User {#3718
name: "Mr. Camryn Zulauf",
email: "llang@example.com",
#password: "password",
first_logged_in_at: "2022-10-05 13:24:58.038",
}
>>> $user->save();
=> true
>>> $user
=> App\Models\User {#3718
name: "Mr. Camryn Zulauf",
email: "llang@example.com",
#password: "password",
first_logged_in_at: "2022-10-05 13:24:58.038",
updated_at: "2022-10-05 13:25:09",
created_at: "2022-10-05 13:25:09",
id: 1,
}
>>> $savedUser = User::find(1);
=> App\Models\User {#4400
id: 1,
name: "Mr. Camryn Zulauf",
email: "llang@example.com",
#password: "password",
created_at: "2022-10-05 13:25:09.000",
updated_at: "2022-10-05 13:25:09.000",
first_logged_in_at: "2022-10-05 13:24:58.038",
}
>>> $user->first_logged_in_at->format('Y-m-d H:i:s.v');
=> "2022-10-05 13:24:58.038"
>>> $savedUser->first_logged_in_at->format('Y-m-d H:i:s.v');
=> "2022-10-05 13:24:58.038"
ばっちり揃いました!ちゃんと保存されていることが確かめられましたね。
$dateFormat
, $dates
を指定する
対処法2:Eloquentモデルに対処法1だと、毎回日時の指定時にフォーマットするのは面倒ですよね。
Eloquentモデルに$dateFormat
, $dates
を指定することで、毎回自分でフォーマットを行う必要がなくなります。
これはハマりポイントですが、Laravel9.xのドキュメントには
データベース内にモデルの日付を実際に保存するときに使用する形式を指定するには、モデルに
$dateFormat
プロパティを定義する必要があります。
と書かれていましたが、記載されていない$dates
も指定しないと、日付の属性と認識してくれずに上手くいかないです。(なんで書かないんだ…)
Laravelの実装コードを読んでいって判明しました。
モデルに以下を追記して再度保存処理を検証します。
/**
* モデルの日付カラムのストレージ形式
*
* @var string
*/
protected $dateFormat = 'Y-m-d H:i:s.v';
/**
* モデルの日付カラムを指定する。
* ここに指定されたものが保存時に$dateFormatで保存される。
* そのため、保存する際の指定でCarbonインスタンスのまま保存しても小数点以下まで保存できる。
*
* @var string[]
*/
protected $dates = [
'first_logged_in_at',
'created_at',
'updated_at',
];
UserFactoryではフォーマットをしません。
public function definition(): array {
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => 'password',
- 'first_logged_in_at' => Carbon::now()->format('Y-m-d H:i:s.v') // <- 日付文字列にフォーマットする
+ 'first_logged_in_at' => Carbon::now() // <- Carbonインスタンスのまま
];
}
tinkerで試してみます。
% docker compose exec laravel.test php artisan tinker
Psy Shell v0.11.8 (PHP 8.1.11 — cli) by Justin Hileman
>>> $user = User::factory()->make();
=> App\Models\User {#4658
name: "Wilber Fadel",
email: "xpfannerstill@example.net",
#password: "password",
first_logged_in_at: "2022-10-05 14:13:31.809",
}
>>> $user->save();
=> true
>>> $user
=> App\Models\User {#4658
name: "Wilber Fadel",
email: "xpfannerstill@example.net",
#password: "password",
first_logged_in_at: "2022-10-05 14:13:31.809",
updated_at: "2022-10-05 14:13:44.511",
created_at: "2022-10-05 14:13:44.511",
id: 1,
}
>>> $savedUser = User::find(1);
=> App\Models\User {#3989
id: 1,
name: "Wilber Fadel",
email: "xpfannerstill@example.net",
#password: "password",
created_at: "2022-10-05 14:13:44.511",
updated_at: "2022-10-05 14:13:44.511",
first_logged_in_at: "2022-10-05 14:13:31.809",
}
上手くいっています。ちなみに、タイムスタンプのcreated_at
とupdated_at
もフォーマットを指定していないですが、小数点以下まで保存されています。
📝 まとめ
普通にCarbonインスタンスから保存する書式にフォーマットすれば、小数点以下も保存できる。(毎回自分でフォーマットするのが面倒・忘れそう)
protected $casts = [
'first_logged_in_at' => 'datetime:Y-m-d H:i:s.v',
];
// これだとミリ秒まで保存されない
User::factory(['first_logged_in_at' => Carbon::now()])->create();
// 自分でフォーマットしたらミリ秒まで保存される
User::factory(['first_logged_in_at' => Carbon::now()->format('Y-m-d H:i:s.v')])->create();
モデルに$dateFormat = 'Y-m-d H:i:s.v';
, $dates = ['first_logged_in_at'];
を指定した場合はCarbonインスタンスを指定するだけで$dateFormat
の書式で保存できる。
// 自分でフォーマットしなくてもミリ秒まで保存できる
User::factory(['first_logged_in_at' => Carbon::now()])->create();
Discussion