首页 > 解决方案 > 对于 Laravel Eloquent 模型及其关系,如何实现由 UUID 组成的主键,而不是自动递增的整数?

问题描述

自动递增整数不能用于存在潜在冲突(冲突)的分布式数据库拓扑中的主键。

关于 UUID 与自动递增整数主题的现有文献是大量的,并且基本规则已被广泛理解。然而,与此同时,似乎不存在关于如何在 Laravel 中实现这一目标的单一、全面的解释,并支持 Eloquent 模型和关系。

VARCHAR(36)下面的文章很有价值,它解释了将主键存储在/中CHAR(36)与通常用于自动递增键的 4/8 字节整数相比所产生的性能开销。我们应该听从这个建议(尤其是作者自始至终注意到的出版后更正):

https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-careful-7b2aa3dcb439

同样有价值的是来自讨论的评论,这是广泛的:

https://news.ycombinator.com/item?id=14523523

下面的文章解释了如何在 Laravel Eloquent 模型中使用 UUID 实现主键,但它没有解释如何为 Eloquent 关系实现相同的功能,例如,带有“数据透视表”的多对多(根据 Laravel 的说法)。

https://medium.com/@steveazz/setting-up-uuids-in-laravel-5-552412db2088

其他人也问过类似的问题,例如Laravel eloquent UUID in a pivot table,但在这种情况下,提问者正在使用 MySQL 触发器生成要插入到数据透视表中的 UUID,我宁愿避免使用纯-雄辩的方法。

How To Cast Eloquent Pivot Parameters中提出了另一个类似的问题?,但问题的关键是如何转换枢轴属性,而不是如何在附加或同步关系时为 ID 列生成自定义值

需要明确的是,我们可以通过将可选的数组参数传递给attach()方法来轻松实现这一点:

->attach($modelAId, $modelBId, ['id' => Uuid::generate()]);

但是每次调用attach()任何一个模型时都必须这样做,这很麻烦并且违反了 DRY 原则。

使用在模型类本身中实现的事件驱动方法会更好地为我们服务。

这种方法可能是什么样的?

标签: phplaravellaravel-5eloquentuuid

解决方案


免责声明:这是一项正在进行的工作。到目前为止,这种技术只关注多对多 Eloquent 关系,而不是更奇特的类型,例如 Has-Many-Through 或 Polymorphics。

当前为 Laravel v5.5.*

Laravel 的 UUID 生成包

在开始之前,我们需要一种机制来生成 UUID。

最流行的 UUID 生成包如下:

https://github.com/webpatser/laravel-uuid

为 Eloquent 模型实现 UUID

模型使用 UUID 作为其主键的能力可以通过扩展 Laravel 的基础 Model 类或通过实现 trait 来赋予。每种方法都有其优点和缺点,因为 Steve Azzopardi 的 medium.com 文章(上面引用)已经解释了 trait 方法(尽管它早于 Eloquent 的$keyType = 'string';属性),我将演示模型扩展方法,当然,它可能是轻松适应一个特质。

无论我们使用模型还是特征,关键方面是$incrementing = false;protected $keyType = 'string';。由于 PHP 的单继承设计,扩展基本 Model 类会施加限制,但它消除了在每个应该使用 UUID 主键的模型中包含这两个关键属性的需要。相比之下,在使用 trait 时,忘记在使用该 trait 的每个模型中都包含这两者会导致失败。

基本 UUID 模型类:

<?php

namespace Acme\Rocket\Models;

use Illuminate\Database\Eloquent\Model;

use Webpatser\Uuid\Uuid;

class UuidModel extends Model
{
    public $incrementing = false;

    protected $keyType = 'string';

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
    }

    public static function boot()
    {
        parent::boot();

        self::creating(function ($model) {
            $model->{$model->getKeyName()} = Uuid::generate()->string;
        });
    }
}

接下来,我们将定义两个模型中的第一个,UserRole,它们以多对多的容量相关。

User型号:

<?php

namespace Acme\Rocket\Models;

use Acme\Rocket\Models\UuidModel;

class User extends UuidModel
{
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
    }
}

这就是任何单个模型将 UUID 用作其主键所需的全部内容。每当创建新模型时,该id列将自动填充新生成的 UUID。

为具有 UUID 主键的模型实现 Eloquent 关系

实现所需行为的必要条件是使用自定义数据透视模型,特别是因为我们需要禁用主键列 ( id) 的自动递增,并将其类型从 更改intstring,就像我们在UuidModel上面的类中所做的那样。

自 Laravel 5.0 以来,自定义枢轴模型已经成为可能,但在最近的版本中使用已经发展。有趣的是,有必要将 5.0 的用法与 5.5+ 的用法结合起来才能使这一切正常工作。

自定义枢轴模型非常简单:

<?php

namespace Acme\Rocket\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    public $incrementing = false;

    protected $keyType = 'string';
}

现在,我们将关系添加到第一个 ( User) 模型中:

<?php

namespace Acme\Rocket\Models;

use Webpatser\Uuid\Uuid;

use Illuminate\Database\Eloquent\Model;

use Acme\Rocket\Models\UuidModel;
use Acme\Rocket\Models\Role;
use Acme\Rocket\Models\RoleUser;

class User extends UuidModel
{
    protected $fillable = ['name'];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
    }

    public function roles()
    {
        return $this->belongsToMany(Role::class)
            ->using(RoleUser::class);
    }

    public function newPivot(Model $parent, array $attributes, $table, $exists, $using = NULL) {
        $attributes[$this->getKeyName()] = Uuid::generate()->string;

        return new RoleUser($attributes, $table, $exists);
    }
}

需要注意的关键元素是方法中的自定义数据透视模型roles()->using(RoleUser::class)以及newPivot()方法覆盖;无论何时编辑模型,都需要将 UUID 插入到数据透视表的id列中attach()

接下来,我们需要定义Role模型,它本质上是相同的,但多对多关系颠倒了:

<?php

namespace Acme\Rocket\Models;

use Webpatser\Uuid\Uuid;

use Illuminate\Database\Eloquent\Model;

use Acme\Rocket\Models\UuidModel;
use Acme\Rocket\Models\User;
use Acme\Rocket\Models\RoleUser;

class Role extends UuidModel
{
    protected $fillable = ['name'];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
    }

    public function users()
    {
        return $this->belongsToMany(User::class)
            ->using(RoleUser::class);
    }

    public function newPivot(Model $parent, array $attributes, $table, $exists, $using = NULL) {
        $attributes[$this->getKeyName()] = Uuid::generate()->string;

        return new RoleUser($attributes, $table, $exists);
    }
}

演示其工作原理的最佳方法是迁移:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

//use Webpatser\Uuid\Uuid;

use Acme\Rocket\Models\User;
use Acme\Rocket\Models\Role;

class UuidTest extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->uuid('id');
            $table->primary('id');
            $table->string('name');
            $table->timestamps();
        });

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

        Schema::create('role_user', function (Blueprint $table) {
            $table->uuid('id');
            $table->primary('id');
            $table->unique(['user_id', 'role_id']);
            $table->string('user_id');
            $table->string('role_id');
        });

        $user = User::create([
            'name' => 'Test User',
        ]);

        $role = Role::create([
            'name' => 'Test Role',
        ]);

        // The commented portion demonstrates the inline equivalent of what is
        // happening behind-the-scenes.

        $user->roles()->attach($role->id/*, ['id' => Uuid::generate()->string]*/);
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('role_users');
        Schema::drop('users');
        Schema::drop('roles');
    }
}

运行上述迁移后,role_user表如下所示:

MariaDB [laravel]> SELECT * FROM `role_user`;
+--------------------------------------+--------------------------------------+--------------------------------------+
| id                                   | user_id                              | role_id                              |
+--------------------------------------+--------------------------------------+--------------------------------------+
| 6f7b3820-6b48-11e8-8c2c-1b181bec620c | 6f76bf80-6b48-11e8-ac88-f93cf1c70770 | 6f78e070-6b48-11e8-8b2c-8fc6cc4722fc |
+--------------------------------------+--------------------------------------+--------------------------------------+
1 row in set (0.00 sec)

为了检索模型和关系,我们将执行以下操作(使用 Tinker):

>>> (new \Acme\Rocket\Models\User)->first()->with('roles')->get();
=> Illuminate\Database\Eloquent\Collection {#2709
     all: [
       Acme\Rocket\Models\User {#2707
         id: "1d8bf370-6b1f-11e8-8c9f-8b67b13b054e",
         name: "Test User",
         created_at: "2018-06-08 13:23:21",
         updated_at: "2018-06-08 13:23:21",
         roles: Illuminate\Database\Eloquent\Collection {#2715
           all: [
             Acme\Rocket\Models\Role {#2714
               id: "1d8d4310-6b1f-11e8-9c1b-d33720d21f8c",
               name: "Test Role",
               created_at: "2018-06-08 13:23:21",
               updated_at: "2018-06-08 13:23:21",
               pivot: Acme\Rocket\Models\RoleUser {#2712
                 user_id: "1d8bf370-6b1f-11e8-8c9f-8b67b13b054e",
                 role_id: "1d8d4310-6b1f-11e8-9c1b-d33720d21f8c",
                 id: "89658310-6b1f-11e8-b150-bdb5619fb0a0",
               },
             },
           ],
         },
       },
     ],
   }

可以看出,我们定义了两个模型并通过多对多关系将它们关联起来,在所有实例中使用 UUID 代替自动递增整数。

这种方法使我们能够避免任何数量的分布式或复制数据库场景中的主键冲突,从而为在未来几十年内良好扩展的大型复杂数据结构铺平道路。

最后的想法

多对多同步方法似乎有效,例如sync()syncWithoutDetaching()toggle(),尽管我没有彻底测试它们。

这不是大型技术的唯一方法,也不太可能是“最佳”方法。虽然它适用于我有限的用例,但我相信其他比我更精通 Laravel 和 Eloquent 的人可以提供改进建议(请这样做!)。

我打算将整体方法扩展到其他关系类型,例如 Has-Many-Through 和 Polymorphics,并将相应地更新此问题。

在 MySQL/MariaDB 中使用 UUID 的一般资源

http://www.mysqltutorial.org/mysql-uuid/

MySQL 中本机 UUID 支持的状态

我的理解是 MySQL 8 只是添加了新功能,使使用 UUID 更容易;它不添加“本机”UUID 数据类型。

并且通过“更容易”,新函数似乎减轻了在VARCHAR(36)/CHAR(36)字符串和BINARY(16)表示之间转换的一些挑战。显然,后者要快得多。

https://mysqlserverteam.com/mysql-8-0-uuid-support/

MariaDB 中本机 UUID 支持的状态

有一个“功能请求”开放以获得更好的 UUID 支持(这张票解释了一些基本原理):

https://mariadb.atlassian.net/browse/MDEV-4958


推荐阅读