如何实现由 UUID 组成的主键,而不是自动递增的整数,用于 Laravel Eloquent 模型及其关系



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

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

以下文章是值得的,它解释了以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 雄辩 UUID ,但在这种情况下,提问者正在使用 MySQL 触发器生成要插入数据透视表中的 UUID,我宁愿避免这样做,而倾向于纯粹的雄辩方法。

另一个类似的问题在如何转换雄辩的透视参数?中提出,但问题的症结是如何转换透视属性,而不是如何在附加或同步关系时为 ID 列生成自定义值

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

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

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

如果在模型类本身中实现的事件驱动方法,我们会得到更好的服务。

这种方法会是什么样子?

免责声明:这是一项正在进行的工作。到目前为止,这种技术只关注多对多雄辩关系,而不是更奇特的类型,如多通或多态。

当前自 Laravel v5.5 起

用于拉拉维尔的UUID生成包

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

最流行的 UUID 生成包如下:

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

雄辩的模型实现 UUID

模型使用 UUID 作为其主键的能力可以通过扩展 Laravel 的基本模型类或通过实现特征来授予。每种方法都有其优点和缺点,并且由于Steve Azzopardi的 medium.com 文章(上面引用)已经解释了特征方法(尽管它早于Eloquent的$keyType = 'string';属性),我将演示模型扩展方法,当然,它可以很容易地适应特征。

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

基本 UUID 模型类:

<?php
namespace AcmeRocketModels;
use IlluminateDatabaseEloquentModel;
use WebpatserUuidUuid;
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 AcmeRocketModels;
use AcmeRocketModelsUuidModel;
class User extends UuidModel
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
}
}

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

使用 UUID 主键实现模型的雄辩关系

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

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

自定义透视模型非常简单:

<?php
namespace AcmeRocketModels;
use IlluminateDatabaseEloquentRelationsPivot;
class RoleUser extends Pivot
{
public $incrementing = false;
protected $keyType = 'string';
}

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

<?php
namespace AcmeRocketModels;
use WebpatserUuidUuid;
use IlluminateDatabaseEloquentModel;
use AcmeRocketModelsUuidModel;
use AcmeRocketModelsRole;
use AcmeRocketModelsRoleUser;
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()方法重写;每当attach()模型时,将 UUID 插入数据透视表的id列都需要这两个元素。

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

<?php
namespace AcmeRocketModels;
use WebpatserUuidUuid;
use IlluminateDatabaseEloquentModel;
use AcmeRocketModelsUuidModel;
use AcmeRocketModelsUser;
use AcmeRocketModelsRoleUser;
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 IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
//use WebpatserUuidUuid;
use AcmeRocketModelsUser;
use AcmeRocketModelsRole;
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 AcmeRocketModelsUser)->first()->with('roles')->get();
=> IlluminateDatabaseEloquentCollection {#2709
all: [
AcmeRocketModelsUser {#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: IlluminateDatabaseEloquentCollection {#2715
all: [
AcmeRocketModelsRole {#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: AcmeRocketModelsRoleUser {#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

最新更新