Как реализовать первичные ключи, состоящие из UUID, вместо автоматически увеличивающихся целых чисел, для моделей Laravel Eloquent и их отношений? - PullRequest
0 голосов
/ 08 июня 2018

Автоинкрементные целые числа нельзя использовать для первичных ключей в топологиях распределенных баз данных, в которых существует потенциальная возможность возникновения конфликтов (коллизий).

Существующая литература, касающаяся предмета UUID, и автоинкрементных целых чиселОбширен, и основные принципы широко поняты.Тем не менее, в то же время, по-видимому, не существует единого всеобъемлющего объяснения того, как этого добиться в Laravel, с поддержкой Eloquent Models и отношений .

Следующая статья заслуживает внимания иобъясняет снижение производительности при хранении первичных ключей в 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

В следующей статье объясняется, как реализовать первичные ключи с использованием UUID в моделях Laravel Eloquent, но не дается объяснение , как реализовать то же самое для отношений Eloquent например, «многие ко многим» с «сводными таблицами» (на языке Laravel).

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

Другие задавали аналогичные вопросы, такие как красноречивый UUID Laravel всводная таблица , но в этом случае запрашивающая сторона генерирует UUID для вставки в сводную таблицу с использованием триггера MySQL, которого я предпочел бы избежать в пользу чисто красноречивого подхода.

Другой похожий вопрос задается в Как привести параметры Eloquent Pivot? , но суть вопроса в том, как привести атрибуты Pivot , а не как генерировать пользовательские значения для столбца ID при присоединении или синхронизации связей .

Чтобы было ясно, мы можем легко добиться этого, передав необязательный аргумент массива в attach() method:

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

Но это будет необходимо делать каждый раз, когда мы вызываем attach() для любой из моделей, что является громоздким и нарушает принцип СУХОГО.

Мы быбыть намного лучше обслуживаемым подходом, управляемым событиями, который реализован в самих модельных классах.

Как может выглядеть такой подход?

1 Ответ

0 голосов
/ 08 июня 2018

Отказ от ответственности: Это незавершенное производство .Пока что эта техника фокусируется только на связях «многие ко многим Eloquent», а не на более экзотических типах, таких как «Has-Many-Through» или «Polymorphics».

Ток с Laravel v5.5.*

Пакеты генерации UUID для Laravel

Прежде чем мы начнем, нам нужен механизм, с помощью которого генерируются идентификаторы UUID.

Наиболее популярный пакет для генерации UUID выглядит следующим образом:

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

Реализация UUID для красноречивых моделей

Возможность для модели использовать UUID в качестве своего первичного ключа может быть предоставлена ​​либо путем расширения базового класса модели Laravel,или путем реализации черты.У каждого подхода есть свои сильные и слабые стороны, и поскольку в статье medium.com Стива Аззопарди (цитированной выше) уже объясняется метод trait (хотя он предшествует свойству $keyType = 'string'; в Eloquent), я продемонстрирую подход к расширению модели, который, конечно,, могут быть легко адаптированы к признаку.

Используем ли мы модель или признак, важнейшими аспектами являются $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;
        });
    }
}

Далее, мы 'Определяем первую из двух моделей, 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);
    }
}

Thisэто все, что требуется для любой отдельной модели, чтобы использовать UUID для своего первичного ключа.Всякий раз, когда создается новая модель, столбец id будет заполняться вновь сгенерированным UUID автоматически.

Реализация красноречивых связей для моделей с первичными ключами 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';
}

Теперь мы добавимотношения с первой (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() ed.

Далее, нам нужно определить модель 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)

И для извлечения моделей и связей мы бы сделали следующее (используя Тинкера):

>>> (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, и обновит этот Вопрос соответствующим образом.

Общие ресурсы для работы с UUID в MySQL / MariaDB

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

Состояние поддержки собственного UUID в MySQL

Насколько я понимаю, MySQL 8 просто добавляет новые функции, которые облегчают работу с UUID;он не добавляет «родной» тип данных UUID.

И благодаря «проще» кажется, что новые функции облегчают некоторые проблемы, связанные с преобразованием между строкой VARCHAR(36) / CHAR(36) и BINARY(16) представительство.По-видимому, последний намного быстрее.

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

Состояние поддержки встроенного UUID в MariaDB

Для лучшей поддержки UUID открыт «Запрос о функции» (этот билетобъясняет некоторые причины)

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

...