jjzjj

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

coder 2024-05-05 原文

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

关于 UUID 与自动递增整数主题的现有文献非常多,基本原则已被广泛理解。然而,与此同时,似乎没有关于如何在 Laravel 中实现这一点的单一、全面的解释,支持 Eloquent 模型和关系。

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

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

同样有值(value)的是来自讨论的评论,它是广泛的:

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,我宁愿避免这种做法,而是采用纯粹的 Eloquent 方法。

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

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

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

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

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

这种方法会是什么样子?

最佳答案

免责声明:这是一个正在进行的工作 .到目前为止,这种技术只关注多对多 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 ) 的自动递增,并将其类型从 int 更改为至 string ,就像我们在 UuidModel 中所做的那样类,以上。

自定义枢轴模型 has been possible since Laravel 5.0 ,但是 the usage has evolved in more recent versions .有趣的是,有必要将 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 代替自动递增的整数。

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

最后的想法

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

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

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

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

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

MySQL 中 native UUID 支持的状态

我的理解是 MySQL 8 只是添加了一些新功能,使使用 UUID 更容易;它不会添加“ native ”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

关于php - 对于 Laravel Eloquent 模型及其关系,如何实现由 UUID 组成的主键,而不是自动递增的整数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50766476/

有关php - 对于 Laravel Eloquent 模型及其关系,如何实现由 UUID 组成的主键,而不是自动递增的整数?的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

  5. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  6. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  7. [工业相机] 分辨率、精度和公差之间的关系 - 2

    📢博客主页:https://blog.csdn.net/weixin_43197380📢欢迎点赞👍收藏⭐留言📝如有错误敬请指正!📢本文由Loewen丶原创,首发于CSDN,转载注明出处🙉📢现在的付出,都会是一种沉淀,只为让你成为更好的人✨文章预览:一.分辨率(Resolution)1、工业相机的分辨率是如何定义的?2、工业相机的分辨率是如何选择的?二.精度(Accuracy)1、像素精度(PixelAccuracy)2、定位精度和重复定位精度(RepeatPrecision)三.公差(Tolerance)四.课后作业(Post-ClassExercises)视觉行业的初学者,甚至是做了1~2年

  8. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

  9. ruby - 如何搜索、递增和替换 Ruby 字符串中的整数子字符串? - 2

    我有很多这样的文档:foo_1foo_2foo_3bar_1foo_4...我想通过获取foo_[X]的所有实例并将它们中的每一个替换为foo_[X+1]来转换它们。在这个例子中:foo_2foo_3foo_4bar_1foo_5...我可以用gsub和一个block来做到这一点吗?如果不是,最干净的方法是什么?我真的在寻找一个优雅的解决方案,因为我总是可以暴力破解它,但我觉得有一些正则表达式技巧值得学习。 最佳答案 我(完全)不懂Ruby,但类似这样的东西应该可以工作:"foo_1foo_2".gsub(/(foo_)(\d+)/

  10. ruby-on-rails - 只有当不是 nil 时才执行映射? - 2

    如果names为nil,则以下中断。我怎样才能让这个map只有在它不是nil时才执行?self.topics=names.split(",").mapdo|n|Topic.where(name:n.strip).first_or_create!end 最佳答案 其他几个选项:选项1(在其上执行map时检查split的结果):names_list=names.try(:split,",")self.topics=names_list.mapdo|n|Topic.where(name:n.strip).first_or_create!e

随机推荐