🏷️

Laravel UUID、ULID で遊んでみる

2022/09/22に公開

前書き

Laravel Ver.9.30.1、9.31.0 で、UUID / ULID 関係の機能が追加されましたので、その機能で遊んでみた内容を記事にしたいと思います。

UUID and ULID support for Eloquent #44074
Improve UUID and ULID support for Eloquent #44146

上記の機能追加は、主に、

  • Eloquent モデルに対するトレイトの提供
  • ULID のサポート

という感じになると思います。

なお、UUID / ULID を id の代わりのプライマリキーにしたり、又は id とは別の項目として持たせる事も上記のトレイトでは対応可能になっています。

という事で、まずは予習(or 復習)から。

UUID / ULID とは?

詳しくは下に掲げた参考リンクなどを参照いただきたいのですが、私なりにざっくりまとめさせていただきますと…。

UUID(Universally Unique Identifier)とは、ソフトウェア上でオブジェクトを一意に識別するための識別子である。UUIDは128ビットの数値だが、16進法による550e8400-e29b-41d4-a716-446655440000というような文字列による表現が使われることが多い。(wikiより)

ざっくり、重複する恐れは天文学的に低いので、安心して使用できるIDという感じですね。RFC 4122として規格化されています。

一方、ULID(Universally Unique Lexicographically Sortable Identifier)は、コミュニティ(or 草の根)的な規格で、UUIDと互換性を持たせる形で作られつつも、UUID Ver.4 では、ソートできないのに対し、ULID は、先頭48ビットにタイムスタンプを置くことにより、ソート可能となっている、という感じですね。

また UUID は、16進表記の32文字で表現されるのに対し、ULID は、26文字のエンコードされた文字で表現されるという違いがあります。例えば、下記みたいな感じです。

UUID(Ver.4)
ca004cbd-a728-4232-90db-d9a2a31d3c80

ULID
01GDFVRZW5YKKGDFTFQCM6BDF5

ULID の短いのは良いですね。ですが、UUID も何かちょっと格好良い?

参考リンク(参考にさせていただいた記事等)

https://ja.wikipedia.org/wiki/UUID
https://qiita.com/kai_kou/items/b4ac2d316920e08ac75a
https://blog.daveallie.com/ulid-primary-keys
https://techblog.raccoon.ne.jp/archives/1627262796.html
https://qiita.com/kawasima/items/6b0f47a60c9cb5ffb5c4
https://asnokaze.hatenablog.com/entry/2021/04/28/030550
https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7

UUID のバージョン

UUID には、バージョンがあり、現在は Ver.4 が主流と言えます。UUID Ver.4 は、バージョン情報などの6ビットを除けば、完全ランダムな値になっています。

ですが、ここに来て、UUID を生成する PHP 用のライブラリが、Ver.7 に対応されてきました。
Ver.7 は、ULID のように先頭にタイムスタンプ情報を持つことにより、ソート可能な ID が生成されるようになっています。

Laravel の Ver.4 と言いつつソート可能な Ver.7風 UUID

UUID Ver.4 は、本来、完全ランダムな値なのですが、Laravel では、ソート可能な UUID Ver.4(Ver.7風)が利用可能になっています。

(Ver.4 と言いつつ、完全ランダムではないので、もはや Ver.4 の規格と違うのではないか、という噂もありますが…。個人的には、これが一体何なのか、よく分かっていません😅 Laravel オリジナルという感じ❓)

本題

という事で、UUID / ULID を試してみたいと思います。
まずは、執筆時点の最新の Ver.9.31.0 をインストールします。

本題に進む前に、今回試す内容を書きたいと思います。

  1. UUID Ver.4 を今回の新機能を使わずに試す
  2. UUID Ver.4 を今回の新機能を使って書き直す
  3. UUID Ver.7 を使ってみる
  4. ULID を使ってみる

となります。
UUID / ULID は、id の代わりのプライマリキーとして作成してみたいと思います。

1. UUID Ver.4 を今回の新機能を使わずに試す

実は Laravel では、Str::uuid() を呼び出す事で、UUID を得ることができるようになっています。

改めて調べて見ると、、、Ver.5.6 の時から利用できるようですね。
https://github.com/laravel/framework/commit/3d39604bba72d45dab5b53951af42bbb21110cad

上記のプルリク見ても分かる通り、Str::uuid() と Str::orderedUuid() の2つがあり、前者が本来の UUID Ver.4 、後者は、Ver.4 と言いつつもソート可能な Ver.7風 UUID です。
ここでは、Str::uuid() を使っていきます。

と言うことで、まずは Post モデルを作ります。

php artisan make:model Post -m
postマイグレーション
        Schema::create('posts', function (Blueprint $table) {
            $table->uuid('uuid')->primary();
            $table->string('title');
            $table->longText('body');
            $table->timestamps();
        });

Ver.9 から uuid() の第1引数は省略可能で、デフォルトは、uuid となります。
(Larastan 使う場合、明示した方が良いです)

マイグレーションを実行し

php artisan migrate

下記みたいに書きます。

Post.php
use Illuminate\Support\Str;

class Post extends Model
{
    use HasFactory;

    protected $primaryKey = 'uuid';

    protected $keyType = 'string';

    public $incrementing = false;

    protected static function booting()
    {
        static::creating(function ($post) {
            if (blank($post->uuid)) {
                $post->uuid = (string) Str::uuid();
            }
        });
    }
}

(Docblock や細かい説明は紙面の都合上、割愛させていただきます😅)

booting() メソッドを利用する事で、モデルを生成する際に、UUID を設定しています。
(string) の箇所と Str の名前空間のインポートも忘れないようにします。
続けて、web.php を下記みたいにします。

web.php
Route::get('/', function () {
    $post = new Post;
    $post->title = 'タイトル';
    $post->body = '本文';
    $post->save();

    dd($post->toArray());
});

これで、トップページにアクセスすると、下記みたいに出力されます。

^ array:5 [▼
  "title" => "タイトル"
  "body" => "本文"
  "uuid" => "b55103c3-8534-4c6b-90ff-dcba3693968b"
  "updated_at" => "2022-09-21T12:42:38.000000Z"
  "created_at" => "2022-09-21T12:42:38.000000Z"
]

uuid が、ちゃんと設定されていますね。
記述は省きますが、もちろん「ルートモデル結合」も利用できます。

2. UUID Ver.4 を今回の新機能を利用して書き直す

さて、UUID を色んなモデルで使うとなると、トレイト化したくなったりします。そこで今回、そのトレイトが提供されました。

上記で見たのを下記みたいにトレイトを利用する事で、すっきり書く事ができます。

Post.php
use Illuminate\Database\Eloquent\Concerns\HasUuids;

class Post extends Model
{
    use HasFactory;

    use HasUuids;

    protected $primaryKey = 'uuid';

少しスッキリしましたね。
uuid という項目名ではなく、id としておけば、最後の1行も省けます。

ただこれ、先程の手動で書いた場合と少し異なる点があります。
それが何かと言うと、HasUuids トレイトの中では、UUID の生成に Str::orderedUuid() が使われています。完全ランダムではなく、ソート可能なやつです。

下記のイメージをご確認下さい。それぞれ5つずつ列挙しています。
(ついでに、UUID Ver.7 や ULID も記述しています)

// UUID Ver.4(先頭部分もランダム)
04aff9a5-b6be-466f-a6a2-e89b165bbf22
433957cd-8e6c-4a13-953b-77dfecdb8227
4c33fae2-c121-4250-bd55-37853b52a7ee
7b762ffb-52e3-4ff9-b327-00f77d43ed0a
f23916ce-9be2-4671-8f12-cbd418e22325

// UUID Ver.4 ソート可能 Ver.7 風
97518ca7-8dcf-49f5-888d-262bf859f62a
97518ca8-8394-4f0f-a4ea-1c19e6daa4e3
97518ca9-6b63-4436-a2ce-e57d1c3f05c3
97518caa-58f3-45bc-86b1-b81ab2155104
97518cab-48ef-4cb5-87c0-ed32d060e7d7

// UUID Ver.7
0183601f-527f-7b88-9c2e-09bc7cb91ce1
0183601f-84a0-707b-9592-3b73d50605d3
0183601f-879e-7acf-acd0-b964010ab414
0183601f-89d7-734c-9390-8a38e20db727
0183601f-8be7-70a0-9a6c-e0d9950db411

// ULID
01gdg1nrg84mx1gcs4mavcgqth
01gdg1t4vyxwqavp6wt3zb7fkb
01gdg1t5bp1dtmerx7n9kzv39z
01gdg1t5snqrk0r2kfre02687p
01gdg1t67ncvdt985f52b04acd

3. UUID Ver.7 を使ってみる

今回の新機能が加わるのと前後して、Laravel が UUID の生成に利用しているライブラリ(ramsey/uuid)が、UUID Ver.7 に対応しました。

こうなると、Ver.7 も使ってみたくなりますので、それに対応させたいと思います。Laravel のドキュメント的には、newUniqueId() を上書しろとありますので、それに従うと下記みたいに書けます。

Post.php
use Ramsey\Uuid\Uuid; // これも

class Post extends Model
{
    public function newUniqueId()
    {
        return (string) Uuid::uuid7();
    }
}

あと、もう1つの選択肢としては、Str::uuid()、Str::orderedUuid() の挙動を変えるメソッドが用意されていますので、そちらを使う手もあります。

AppServiceProvider
use Illuminate\Support\Str;
use Illuminate\Support\ServiceProvider;
use Ramsey\Uuid\Uuid;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Str::createUuidsUsing(fn () => (string) Uuid::uuid7());
    }
}

Str::createUuidsUsing() は、上記2つのメソッドの挙動を変える仕様になっています。

ちなみに、Laravel Ver.10 では、UUID のバージョンは、デフォルト Ver.7 に変更になりますね。
[10.x] Switch to UUID v7 #44210
(追記)リバートされたようです。
[10.x] Stick with ordered uuid #44311
[10.x] Revert UUID v7 changes #45546

4. ULID を使ってみる

今度は、Article モデルでやってみます。ULID の生成には、裏では、symfony/uid というライブラリが使われています。

php artisan make:model Article -m
articleマイグレーション
        Schema::create('articles', function (Blueprint $table) {
            $table->ulid('ulid')->primary();
            $table->string('title');
            $table->longText('body');
            $table->timestamps();
        });

ulid() の第1引数のデフォルトは、Laravel Ver.9 では、uuid になっています。(ulid ではなく)Laravel Ver.10 からは、ulid になりました。いずれにしても第1引数は必ず指定するようにしましょう。

Article.php
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasFactory;

    use HasUlids;

    protected $primaryKey = 'ulid';
}
web.php
Route::get('/article', function () {
    $article = new Article();
    $article->title = 'タイトル';
    $article->body = '本文';
    $article->save();

    dd($article->toArray());
});

/article にアクセスすると、下記みたいに出力されます。

^ array:5 [▼
  "title" => "タイトル"
  "body" => "本文"
  "ulid" => "01gdg1nrg84mx1gcs4mavcgqth"
  "updated_at" => "2022-09-21T12:51:58.000000Z"
  "created_at" => "2022-09-21T12:51:58.000000Z"
]

簡単ですね。

あとがき

と言うことで、UUID / ULID で遊んでみました。Ver.4 / Ver.7 / ULID とかありますが、どれを使うかは、パフォーマンスや文字列の長さなど考慮しつつ状況に応じて決める感じになりますかね。

ちなみに、執筆時点においては、テストの際に役立つ freezeUuids() や createUuidsUsingSequence() なんかは、UUID にしか対応していませんね🥲
Add additional uuid testing helpers #42619

間違い等ありましたら、コメントお願いします。

Discussion