ULID とは Universally Unique Lexicographically Sortable Identifier の略で、世界中で重複する可能性が極めて低く辞書的に並び替え可能な識別子のことです。ざっくりいうと時系列でソート可能で衝突を気にしなくていいランダムな値です。これを使ってDB(データベース)中のレコードを識別できる様にすると単純なランダム値をキーにするよりもパフォーマンスが遥かに良く、オートインクリメントする int 型のキーで実現するのが手間だったり不可能だったりした機能の実装がずっと手軽になります。int 型の主キーに比べた時に劣るのはデータの大きさです。たいていの場面では問題ないのですが、大量のデータを ID 付きでやりとりすることを考えると ID の長さの違いはパフォーマンスに影響を与えかねません。
ulid/spec: The canonical spec for ulid
パフォーマンスについては次の記事がわかりやすく詳しいです。
MySQLでプライマリキーをUUIDにする前に知っておいて欲しいこと | Raccoon Tech Blog [株式会社ラクーンホールディングス 技術戦略部ブログ]
Laravel のマイグレーションとモデルで ULID を用いるためのメソッドは主に 9.30、9.31 で追加されました。比較的新しい機能で最近もちょくちょく更新されています。
[9.x] UUID and ULID support for Eloquent by driesvints · Pull Request #44074 · laravel/framework
[9.x] Improve UUID and ULID support for Eloquent by driesvints · Pull Request #44146 · laravel/framework
具体的には次の様に使います。
<?php
/** マイグレーション例 */
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
return new class() extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('users', static function (Blueprint $table) {
// よくある DB のカラムの型を決めるメソッドの部分で ulid メソッドを呼ぶだけ
$table->ulid('user_id')->primary();
$table->string('name')->comment('名前');
$table->dateTime('created_at')->nullable()->comment('作成日時');
$table->dateTime('updated_at')->nullable()->comment('最終更新日時');
$table->dateTime('deleted_at')->nullable()->comment('削除日時');
});
DB::statement("ALTER TABLE users COMMENT 'ユーザー'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
<?php
namespace App\Models\Eloquents;
use Eloquent;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
/**
* App\Models\Eloquents\User
*
* @property string $user_id
* @property string $name 名前
* @property Carbon|null $created_at 作成日時
* @property Carbon|null $updated_at 最終更新日時
* @property Carbon|null $deleted_at
*
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static Builder|User onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|User query()
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserId($value)
* @method static Builder|User withTrashed()
* @method static Builder|User withoutTrashed()
* @mixin Eloquent
*/
class User extends Model
{
use SoftDeletes;
// HasUlids という trait を使うと、主キーを ULID としてよしなに扱ってくれます。
// ULIDの自動生成やオートインクリメント関連の誤爆防止などをしてくれます。
use \Illuminate\Database\Eloquent\Concerns\HasUlids;
public $table = 'users';
protected $primaryKey = 'user_id';
public $guarded = [
'user_id',
'created_at',
'updated_at',
'deleted_at',
];
}
このテーブル定義とモデルに従ってコードを実行すると次の様になります。
<?php
Route::get('/', static function() {
$user = new \App\Models\Eloquents\User();
$user->name = 'ほげ太郎';
$user->save();
dump($user->toArray());
/**
array:4 [▼ // routes/web.php:7
"name" => "ほげ太郎"
"user_id" => "01gpzeq72w0wf9h6f54stna61v"
"updated_at" => "2023-01-17T08:49:50.000000Z"
"created_at" => "2023-01-17T08:49:50.000000Z"
]
*/
$user = new \App\Models\Eloquents\User();
// HasUlids トレイトの中には ULID を生成する newUniqueId メソッドが含まれています。
$user->user_id = $user->newUniqueId();
dump($user->user_id); // "01gpzf431sk2j9prrk57e1h6a3" // routes/web.php:21
$user->name = 'ULID自分で定義太郎';
$user->save();
dump($user->toArray());
/**
array:4 [▼ // routes/web.php:24
"user_id" => "01gpzf431sk2j9prrk57e1h6a3"
"name" => "ULID自分で定義太郎"
"updated_at" => "2023-01-17T08:56:52.000000Z"
"created_at" => "2023-01-17T08:56:52.000000Z"
]
*/
});
特に何もなければ自動で ULID を主キーに保存し、もし既にキーが代入されているならばそちらを優先して保存してくれます。
ULID を用いることで実装が容易になる機能の一つがクライアント側で ID をあらかじめ決定することによる処理の高速化です。あらかじめクライアント側でIDを定義することによって新規登録や複製といった保存操作の完了を待たずに、保存対象を用いた次の操作をするという機能が容易に実装できます。もし ID をサーバーから得る必要があるならば、レスポンスを待つか、仮IDを割り当ておき後で置き換えるといった方法を取ることになります。
また DB 全体においてユニークな値が ID になっているため、気づかれず放置された誤った JOIN 、複数のテーブルを背景に持つ1モデルの取り扱い、ポリモーフィックリレーションと外部キー制約とクエリの作りやすさの兼ね合い、といった諸々の問題も解決しやすくなります。