Laravel の主キーで UUID を利用する時にハマった事、調べた事

起きた問題

テーブルの id カラムを UUID で登録できるようにしていました。

例えば、以下のような users テーブルがあるとします。

テーブル名:users

id name
62ce6b67-f8e3-4279-a2a1-0a23f3ca5730 はてな 太郎
830fd685-58dc-46b5-b7df-16e06f3c1b34 はてな 花子

単純にユーザーの一覧を取得したく、User::all() を実行してみたところ、id が上記のような 36 桁の文字列ではなく、以下のように id が欠落された状態で取得されてしまいました。

{
    "data": [
        {
            "id": "62",
            "name": "はてな 太郎"
        },
        {
            "id": "830",
            "name": "はてな 花子"
        }
    ]
}

これを解決すべく調べた事や、試した事などをまとめます。

原因

まさにコレだ!というものが Laravel のドキュメントに書かれていました。

Eloquentは主キーを自動増分される整数値であるとも想定しています。つまり、デフォルト状態で主キーは自動的にintへキャストされます。

Eloquent:利用の開始 5.8 Laravel の「主キー」

この仕様のため、各 id の最初の数字部分(83062)だけが取得された事になります。

私はコレで解決しました

上記の Laravel のドキュメントと同じ箇所にこのように書かれていました。

自動増分ではない、もしくは整数値ではない主キーを使う場合、モデルにpublicの$incrementingプロパティを用意し、falseをセットしてください。主キーが整数でない場合は、モデルのprotectedの$keyTypeプロパティへstring値を設定してください。

これを読んだ時に正直思った事は、 $incrementing$keyType の両方を設定しないといけないの?それとも片方だけでいいの?でした。

まずは、他の方のサイトも参考にしつつ、両方のプロパティを設定する事にしました。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public $incrementing = false; // ← 追加

    protected $keyType = 'string'; // ← 追加

    protected $fillable = ['id', 'name'];
}

これらのプロパティを追加後にデータ取得し直してみると・・・

{
    "data": [
        {
            "id": "62ce6b67-f8e3-4279-a2a1-0a23f3ca5730",
            "name": "はてな 太郎"
        },
        {
            "id": "830fd685-58dc-46b5-b7df-16e06f3c1b34",
            "name": "はてな 花子"
        }
    ]
}

といった具合に、今度は id の一部が欠落されることなく取得することが確認できました。

以上です。。。

だけだとつまらないので、最初に思った疑問「プロパティ両方必要?それとも片方だけ?」を少し試しました。

プロパティ両方必要?それとも片方だけ?

結果を先に言いますと、どちらか片方だけでも想定通りの動き(id がちゃんと文字列 36 桁で取得)をしました。

    // public $incrementing = false;

    protected $keyType = 'string';

でも

    public $incrementing = false;

    // protected $keyType = 'string';

データの登録、取得で問題なく動きました。

しかし、なんだか腑に落ちないのでもう少し調べてみました。

Model に定義されているデフォルト値

Laravel の Illuminate\Database\Eloquent\Model の中身を見てみたところ、以下のようにデフォルト値が設定されていました。

なので、どちらかのプロパティを設定して想定通りに動いたとしても、そのモデルのプロパティ値が気持ち悪い状態だな、と思いました。

$incrementing は false で、$keyType は int であれば id に自前で用意した数値を入れるんだな、という感じですが、 $incrementing は true だけど $keyType は string の時に、矛盾しているように感じました。

    /**
     * The "type" of the auto-incrementing ID.
     *
     * @var string
     */
    protected $keyType = 'int';

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = true;

そこで、これらのプロパティが使われている(何かしらの条件で使われている)箇所を調べてみたところ、以下のような処理が実行されていることが分かりました。

Model クラスの 782 行目(私の環境では)

        // If the model has an incrementing key, we can use the "insertGetId" method on
        // the query builder, which will give us back the final inserted ID for this
        // table from the database. Not all tables have to be incrementing though.
        $attributes = $this->getAttributes();

        if ($this->getIncrementing()) {
            $this->insertAndSetId($query, $attributes);
        }

        // If the table isn't incrementing we'll simply insert these attributes as they
        // are. These attribute arrays must contain an "id" column previously placed
        // there by the developer as the manually determined key for these models.
        else {
            if (empty($attributes)) {
                return true;
            }

            $query->insert($attributes);
        }

$this->getIncrementing() は、モデルの $incrementing の値を返しています。

そのため、例え $keyType が string に設定していて動いていても、この insertAndSetId が実行されてしまいそうです。 これ以上先は追いきれませんでした・・・。

insertAndSetId が実行されても問題なく動くのですが、ムダな処理が実行されてしまいそうです。

なのでやはり id に UUID を登録したい時には、両方のプロパティを設定した方が良さそうに思いました。

補足

Laravel のマイグレーションファイルで、id を UUID が登録できるようにするために、以下のように定義してテーブルを作成しました。

Schema::create('users', function (Blueprint $table) {
    $table->string('id', 36)->primary();
    $table->string('name');
    $table->timestamps();
});

まとめ

今回はちょっとした事をきっかけに、フレームワークの中身を少しだけですが追ってみました。

ソースコードを目で見て追うだけでなく、実際にどう動いているのかを確認できたら面白いんだろうな、と感じました。

この辺は次なる課題として、、、もっと手を動かして実験して、まとめていけるようになりたいなと思います。