Ben*_*son 5 php uuid laravel eloquent laravel-5
自动递增整数不能用于存在潜在冲突(冲突)的分布式数据库拓扑中的主键。
关于 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()]);
Run Code Online (Sandbox Code Playgroud)
但是每次我们调用attach()任何一个模型时都需要这样做,这很麻烦并且违反了 DRY 原则。
我们会更好地使用在模型类本身中实现的事件驱动方法。
这种方法会是什么样子?
免责声明:这是一项正在进行的工作。到目前为止,该技术仅关注多对多 Eloquent 关系,而不是更奇特的类型,例如 Has-Many-Through 或多态性。
在开始之前,我们需要一种生成 UUID 的机制。
最流行的 UUID 生成包如下:
https://github.com/webpatser/laravel-uuid
模型使用 UUID 作为其主键的能力可以通过扩展 Laravel 的基本 Model 类或通过实现特征来赋予。每种方法都有其优点和缺点,并且因为 Steve Azzopardi 的medium.com 文章(上面引用)已经解释了特征方法(尽管它早于 Eloquent 的$keyType = 'string';属性),所以我将演示模型扩展方法,当然,这可能是轻松适应某种特质。
无论我们使用模型还是特征,关键的方面是$incrementing = false;和protected $keyType = 'string';。虽然扩展基 Model 类由于 PHP 的单继承设计而受到限制,但它消除了在每个应使用 UUID 主键的模型中包含这两个关键属性的需要。相比之下,当使用特征时,忘记在使用该特征的每个模型中都包含这两者将导致失败。
基本 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;
});
}
}
Run Code Online (Sandbox Code Playgroud)
接下来,我们将定义两个模型中的第一个User和Role,它们以多对多的方式相关。
该User模型:
<?php
namespace Acme\Rocket\Models;
use Acme\Rocket\Models\UuidModel;
class User extends UuidModel
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
}
}
Run Code Online (Sandbox Code Playgroud)
这就是任何单个模型使用 UUID 作为其主键所需的全部内容。每当创建新模型时,该id列都会自动填充新生成的 UUID。
实现所需行为的必要条件是使用自定义数据透视模型,特别是因为我们需要禁用主键列 ( id) 的自动增量,并将其类型从 更改int为string,就像我们在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';
}
Run Code Online (Sandbox Code Playgroud)
现在,我们将向第一个 ( ) 模型添加关系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);
}
}
Run Code Online (Sandbox Code Playgroud)
roles()需要注意的关键元素是方法中的自定义枢轴模型->using(RoleUser::class),以及newPivot()方法重写;id无论何时编辑模型,都需要将 UUID 插入到数据透视表的列中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);
}
}
Run Code Online (Sandbox Code Playgroud)
演示其工作原理的最佳方法是迁移:
<?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');
}
}
Run Code Online (Sandbox Code Playgroud)
运行上述迁移后,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)
Run Code Online (Sandbox Code Playgroud)
为了检索模型和关系,我们将执行以下操作(使用 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",
},
},
],
},
},
],
}
Run Code Online (Sandbox Code Playgroud)
可以看到,我们定义了两个模型,并通过多对多关系将它们关联起来,在所有实例中使用 UUID 代替自动递增整数。
这种方法使我们能够避免在任意数量的分布式或复制数据库场景中出现主键冲突,从而为未来几十年能够良好扩展的大型复杂数据结构铺平道路。
多对多同步方法似乎有效,例如sync()、syncWithoutDetaching()、 和toggle(),尽管我还没有彻底测试它们。
这不是实现更大技术的唯一方法,而且不太可能是“最佳”方法。虽然它适用于我有限的用例,但我确信其他比我更精通 Laravel 和 Eloquent 的人可以提供改进建议(请这样做!)。
我打算将整体方法扩展到其他关系类型,例如Has-Many-Through和多态性,并将相应地更新这个问题。
http://www.mysqltutorial.org/mysql-uuid/
我的理解是 MySQL 8 只是添加了新功能,使使用 UUID 变得更容易;它不添加“本机”UUID 数据类型。
通过“更简单”,新函数似乎减轻了VARCHAR(36)/CHAR(36)字符串和BINARY(16)表示形式之间转换的一些挑战。显然,后者要快得多。
https://mysqlserverteam.com/mysql-8-0-uuid-support/
有一个“功能请求”开放,以获得更好的 UUID 支持(此票证解释了一些基本原理):
https://mariadb.atlassian.net/browse/MDEV-4958