在 Laravel 中实现 UUID 主键,以及它的好处
嘿,DEV.to 社区!
我已经很久没有在这里写东西了。
在我缺席的这段时间里,我一直在从事一些大型项目,在这些项目中我看到了 UUID 如何以多种方式使您的应用程序受益。
我已经使用 UUID 作为我的主键结构相当长一段时间了,但真正的优势是在完成项目后才显现出来的,我很高兴我决定这样做。
我主要使用 Laravel 作为后端,并且喜欢使用 MySQL(我知道市面上有很多其他的关系型数据库管理系统 (RDBMS)/数据库管理系统,但我还是喜欢 MySQL,用起来感觉很顺手 XD)。虽然在使用 MySQL 或许多其他数据库时,你的主键会被设置为 AI(自增),数据类型也会是整数,但你只需稍加调整就可以使用 UUID,相信我,这绝对值得。
好处
枚举漏洞修复
枚举漏洞是指你的数据是可预测的。比如说,你在 YouTube 上上传了一个视频,该视频对公众隐藏,只有你分享链接的人才能观看。我们来看一个 YouTube 链接:
https://www.youtube.com/watch?v=3wVTmlD86a
如您所见,地址以 结尾,v=3wVTmlD86a
表示视频的 ID(假设是 ID)。想象一下,YouTube 使用数字索引,并针对每个上传的视频逐一递增。那么您可能会问,在这种情况下,有人可以从 1 开始,使用定制程序尝试每个链接,以提取 YouTube 上所有可用的视频,而您的纯链接视频也会被暴露。
这被称为枚举利用,它可能会导致严重的数据泄露,具体取决于您的程序应该做什么。
YouTube 为其视频制作 ID 的算法是自定义的,但我的描述的目的与 UUID 相同。
给定一个 UUID,例如700234f5-0e45-452e-ae3a-70b4b3d024e1
,由于没有顺序,你无法知道它之前或之后的 UUID 是什么。正如你所见,这可以解决应用程序中的枚举漏洞。
可水平扩展的数据库
假设您有同一系统的两个数据库实例,出于某种原因,您想要扩展它们或创建一个新的集群,然后合并它们。由于数据库结构相同,并且数据库中的关系复杂,如果不手动或使用自动化任务调整某些值,合并数据可能会很困难。
正如您在上面的抽象数据库简单图中所看到的,您可能有一个与产品和用户相关的订单表。
现在让我们考虑一下:在每个数据库实例中,您只有 10 个用户、10 个产品和 10 个订单,它们的数字索引从 1 到 10。假设在我们的第一个数据库实例中,订单 2 与用户 3 相关,其名称是Adnan Babakan
。在另一个实例中,订单 2 也与用户 3 相关,但他们的名称是Arian Amini
。即使我们正确导入订单,并且它们的 ID 没有从其他表中引用,并且它们的索引从 1 重新分配到 20,由于用户也重新建立了索引,现在我们的订单引用了错误的用户!
我想你现在应该明白问题和解决方案了。如果我们使用 UUID,由于它们是全局唯一的,所以我们的数据之间不会发生冲突,而且我们可以合并任意数量的实例,没有任何问题。
除了合并两个数据库之外,UUID 还可以更轻松地管理基于集群的数据库架构以及同时运行的多个数据库实例。
缺点
在数据库中使用 UUID 有两个主要缺点。首先是它会占用数据库空间,其次是插入问题。
由于 UUID 不是有序的,因此执行插入操作的成本会很高,因为使用数字索引系统时,您的记录将被插入到随机位置,而不是插入到表的末尾。
尽管存在这些缺点,但这也是可以原谅的,因为空间并不是那么昂贵,而且在大多数情况下插入问题也不是什么大问题。
有一种方法可以避免插入问题,即使用与以前相同的数字索引系统,但也为每条记录分配单独的 UUID,这样您就可以从前面提到的要点中受益。
在 Laravel 中使用 UUID
在本文的这一部分,我将讨论如何在 Laravel 应用程序中实现 UUID,以及我遇到的问题和解决方法。请记住,UUID 并非仅适用于 Laravel/MySQL,您可以在任何其他编程语言、框架和数据库中使用它们。
请记住,这是我实现这种方法的首选方法,您可能会找到更有效的方法,如果您这样做了,请在评论中告诉我。
迁移
您需要做的第一件事是知道如何定义迁移,以便您的表使用 UUID。
幸运的是,Laravel 提供了许多好的方法和工具来帮助您实现目标。
首先,让我们改变一下在迁移中定义 PK(主键)的方式。在 Laravel 迁移中,你通常这样定义 PK:
$table->id();
上面的代码等同于:
$table->integer('id')->primary();
遗憾的是,在迁移中没有创建基于 UUID 的主键的简便方法。不过这也不是什么大工程:
$table->uuid('id')->primary();
太棒了!现在你的表将有一个名为 的列,id
其中包含 UUID。
参考和关系
现在我们来谈谈关系。关系是指您想要引用另一个表或同一个表中的不同记录。引用使用目标记录的 ID 来完成,因为它是唯一的键,您确信它永远不会重复。
假设您想要在表中引用用户,这就是您在迁移中定义正常关系的方式:
$table->foreignId('user_id')->constrained();
请记住,该constrained()
方法是以下内容的简写:
$table->foreignId('user_id')->references('id')->on('users');
Laravel 赋予了你简写的能力,但代价是需要遵循命名约定。因此,当你的外部 id 列被命名user_id
并constrained()
在之后被调用时,Laravel 知道你的意思是这user_id
将引用表id
中的列users
。如果不是这样,你必须使用references()
和on()
方法来明确地定义它,以明确你的意思。
要引用 UUID 类型的外部 ID,可以使用foreignUuid()
method 而不是foreignId()
method。其余操作相同,您也可以将其应用于constrained()
外部 UUID 引用。假设users
表使用 UUID 作为其主键结构,这就是引用用户记录的方法。
$table->foreignUuid('user_id')->constrained();
或者更详细地说:
$table->foreignUuid('user_id')->references('id')->on('users');
模型
现在您已经成功定义了迁移,您需要对模型进行一些小的调整。
模型的默认值告诉它使用基于整数的 ID,而我们应该告诉模型事实并非如此。
首先,添加一个$keyType
以您的模型命名的私有属性并将其值设置为'string'
:
protected $keyType = 'string';
这告诉模型你的键是一种字符串类型而不是整数(UUID 是字符串)。
要做的第二件事是告诉模型不要对这种类型的键使用递增系统,这是通过将$incrementing
属性设置为false
:
public $incrementing = false;
完成这些之后,您的模型就会完美地了解您的 PK 如何运作。
可是等等!创建新记录时,您是否遇到一个问题:该记录id
不能为空且没有默认值?
要解决这个问题,您必须在每次创建新记录时定义一个 UUID,如下所示:
$new_user = new User();
$new_user->id = Str::uuid();
$new_user->username = 'Adnan';
$new_user->password = Hash::make('helloWorld');
$new_user->save();
该类Str
从 导入Illuminate\Support\Str
。Str::uuid()
助手会为您创建一个新的 UUID。
或者您可以使用模型事件来告诉 Laravel 在数据库上创建记录时如何为您的模型创建 ID。
只需在方法中使用闭包事件即可轻松实现booted
。添加一个静态方法,调用booted
模型:
public static function booted() {
}
creating
然后,您可以使用其中调用的事件并告诉它创建一个 UUID 并将其分配给您的模型的 id:
public static function booted() {
static::creating(function ($model) {
$model->id = Str::uuid();
});
}
您可以在https://laravel.com/docs/10.x/eloquent#events-using-closures上阅读有关模型事件的更多信息
如果您使用旧boot
方法,请记住调用父级的boot
方法,以免它被覆盖:
public static function boot() {
parent::boot();
static::creating(function ($model) {
$model->id = Str::uuid();
});
}
如果您已经遵循了到目前为止的步骤,那么您的User
模型或任何其他模型应该看起来像这样:
class User extends Model {
use HasFactory;
...
protected $keyType = 'string';
public $incrementing = false;
...
public static function boot() {
parent::boot();
static::creating(function ($model) {
$model->id = Str::uuid();
});
}
}
解决圣所问题
如果您尝试使用 Sanctum 发行代币,您可能会发现遇到一个错误,提示您约束失败。这是因为您的personal_access_tokens
表设计为引用基于整数的外部 ID(此表使用了变形,因为它应该能够引用多种类型的模型)。
如果您已经有一个正在运行的项目,则应该使用新的迁移来更改表tokenable_id
中列的类型。personal_access_tokens
首先,创建一个新的迁移:
php artisan make:migration change_tokenable_id_type_in_personal_access_tokens_table
然后在迁移中使用列change()
上的方法tokenable_id
:
$table->foreignUuid('tokentable_id')->change();
这会将数据类型更改tokenable_id
为引用 UUID 而不是整数。
为了使我们的数据库结构统一,我想让我的令牌记录也使用 UUID 作为它们的 ID,因此也添加以下指令:
$table->uuid('id')->primary()->change();
如果运行此迁移时收到错误,doctrine/dbal
请使用 Composer 安装该包。
composer require doctrine/dbal
虽然上面描述的方法是更改tokenable_id
列数据类型的正确方法,但如果您刚刚开始一个新项目,或者您不关心当前数据并愿意刷新迁移(这将删除您的数据),那么还有一种更简单的方法。
只需打开2019_12_14_000001_create_personal_access_tokens_table.php
文件(开头的日期可能不同)并更改:
$table->morphs('tokenable');
到:
$table->uuidMorphs('tokenable');
并改变:
$table->id();
到:
$table->uuid('id')->primary();
然后运行:
php artisan migrate
或者:
php artisan migrate:fresh
如果您之前已经运行过迁移。
现在我们personal_access_tokens
可以引用 UUID,并且它的 id 也是一个 UUID,我们需要让 Laravel 知道如何处理我们的令牌,因为 Sanctum 使用默认模型来处理这些记录。
首先,让我们创建一个新模型,该模型将成为 Sanctum 使用的模型,因为我们已经改变了结构:
php artisan make:model PersonalAccessToken
现在打开创建的模型文件,而不是扩展Model
类,而是通过以下方式扩展类Laravel\Sanctum\PersonalAccessToken
:
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
}
由于我们的类与 Sanctum 的默认 PersonalAccessToken 类使用的类同名,因此您应该在导入时定义别名,或者在extend
关键字后使用绝对命名,例如:\Laravel\Sanctum\PersonalAccessToken
我们必须确保 Laravel 像之前一样知道这个模型是如何工作的。因此,我们应该设置$keyType
,$incrementing
并定义一个事件来为每个记录的 ID 创建一个 UUID。
您的最终PersonalAccessToken
模型应该是这样的:
<?php
namespace App\Models;
...
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
...
class PersonalAccessToken extends SanctumPersonalAccessToken
{
use HasFactory;
public $keyType = 'string';
public $incrementing = false;
....
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->id = (string) Str::uuid();
});
}
...
}
最后一步是将此模型定义为 Sanctum 使用的模型。此步骤可以通过Sanctum::usePersonalAccessTokenModel()
方法完成。此方法应在提供程序文件中调用。我更喜欢AppServiceProvider
位于 的app/Providers/AppServiceProvider.php
。
打开提供程序文件并在方法中添加以下代码boot
:
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
记得正确导入Sanctum
和 你的自定义类。类是从 导入的,你的自定义类是从 导入的。PersonalAccessToken
Sanctum
Laravel\Sanctum\Sanctum
PersonalAccessToken
App\Models\PersonalAccessToken
现在您已在 Laravel 应用程序中完成 UUID 的实现!
希望你喜欢这篇文章,并希望它能帮助你实现目标。如果文章中有任何错误,或者你知道更好的方法,请告诉我。
顺便说一句!可以在这里查看我的免费 Node.js Essentials 电子书:
文章来源:https://dev.to/adnanbabakan/implement-uuid-primary-key-in-laravel-and-its-benefits-55o3