在具有主键类型 UUID 的 Laravel/Eloquent 关系上创建相关模型
Creating related Models on Laravel/Eloquent relations with primary key type UUID
Laravel: 7; PHP: 7.4/8.0
在我的项目中,我有两个相关模型:User
和 TimeAccount
。两者都使用 UUID 作为主键,键名仍然是 id
.
迁移:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary();
// snip...
});
}
// snip...
}
class CreateTimeAccountsTable extends Migration
{
public function up()
{
Schema::create('time_accounts', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('user_id')->nullable()->constrained();
// snip...
});
}
// snip...
}
型号:
class User
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function timeAccount()
{
return $this->hasOne(TimeAccount::class);
}
// snip...
}
class TimeAccount
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function user()
{
return $this->belongsTo(User::class);
}
// snip...
}
IsIdentifiedByUuid 特征:
trait IsIdentifiedByUuid
{
protected static function bootIsIdentifiedByUuid()
{
static::creating(fn ($model) => static::isIdentifiedByUuidCreatingHandler($model));
static::saving(fn ($model) => static::isIdentifiedByUuidSavingHandler($model));
}
public function initializeIsIdentifiedByUuid()
{
$this->keyType = 'string';
}
protected function getUuidColumn(): string
{
return property_exists($this, 'uuid_column') ? $this->uuid_column : 'id';
}
protected static function getNewUuid(): UuidInterface
{
$columnName = app(static::class)->getUuidColumn();
$tableName = app(static::class)->getTable();
$columnDescriptor = "$tableName.$columnName";
$uuid = "";
$query = \DB::table($tableName)
->select($columnDescriptor)
->where($columnDescriptor, $uuid);
$attempts = 0;
do {
if ($attempts >= \App\Constants\Uuid::MAX_ATTEMPTS) {
throw new UuidMaxGeneratorAttemptsExceededException();
}
$uuid = Str::uuid();
$attempts++;
} while ($query->setBindings([ $uuid->toString() ])->count() > 0);
return $uuid;
}
/**
* Handles the creation of a model.
* - Generates new UUID if the UUID column is empty and auto-increment is enabled
*
* @param Model $model
* @throws \App\Exceptions\UuidMaxGeneratorAttemptsExceededException
*/
protected static function isIdentifiedByUuidCreatingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
if ($model->getIncrementing() && !$model->{$columnName}) {
$uuid = static::getNewUuid();
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Generating new UUID for `" . get_class($model) . ": $uuid`"
);
$model->{$columnName} = $uuid->toString();
} else {
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Using existing UUID for `" . get_class($model) . ": $model->{$columnName}`"
);
}
}
/**
* Handles the saving of a Model.
* - Prevents changes to the UUID column
* - Rolls back changed value to old value
*
* @param Model $model
*/
protected static function isIdentifiedByUuidSavingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
$originalUuid = $model->getOriginal($columnName);
if (!is_null($originalUuid) &&
$originalUuid !== $model->{$columnName}
) {
\Log::debug(
"IsIdentifiedByUuid [SAVING]:" .
" Prevented change of UUID for `" . get_class($model) . ":$originalUuid`"
);
$model->{$columnName} = $originalUuid;
}
}
}
现在的问题是:
在创建的挂钩上的用户观察者中,我正在这样做:
$userTimeAccount = $user->timeAccount()->create([
// snip...
]);
到目前为止 - 在切换到 UUID 而不是整数键之前 - 一切正常!据我所知,在保存用户模型之前(显然是在触发 auto-inc 之前)创建相关模型是 eloquent/Laravel 明确允许的(尽管我找不到文档的部分).
切换到 UUID 后,这不再有效,我收到 SQL 错误:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (laravel_torus
.time_accounts
, CONSTRAINT time_accounts_user_id_foreign
FOREIGN KEY (user_id
) REFERENCES users
(id
)) (SQL: insert into time_accounts
(time_account_type_id
, balance
, borrow
, user_id
, id
, updated_at
, created_at
) values (3, 0, 0, 0, a995807b-b7e1-4af3-96de-7c48187f943d, 2021-07-09 12:54:25, 2021-07-09 12:54:25))
SQL美化:
insert into `time_accounts`
(`time_account_type_id`, `balance`, `borrow`, `user_id`, `id`, `updated_at`, `created_at`)
values
(3, 0, 0, 0, 'bad86e70-7496-4b42-b2a1-5cc3c5a4d06c', '2021-07-09 11:19:17', '2021-07-09 11:19:17');
不知何故 Laravel 试图为外键插入零(整数),这显然不起作用。我什至尝试手动将 keyType
设置为 'string'
但没有效果。
我哪里错了?
编辑: 似乎存在竞争条件。 Observer 的 created 钩子将在 User
模型的 created 钩子之前被触发。这会导致缺少 ID - 使用整数和自动递增时会出现 ID,正如它应该的那样。
TL;DR
I was not able to solve this with MySQL and switched to Postgres SQL.
DONT USE UUID WITH MYSQL unless you are, absolutely sure that: 1. you need to and 2. the performance impact will be negligible!
我真的无法解释这个错误是怎么发生的。有问题的 ID 已经存在(使用调试器查看模型的 $attribnutes
数组),调用 $user->id
时仍然 Laravel 将 return 整数 0
。
在调查问题时,我注意到 Laravel 正在为 UUID 而不是二进制创建字符串列(这是我所期望的)。由于与 integer/binary 比较相比,字符串比较的成本非常高,所以我决定放弃 MySQL。 UUID 对于此项目是强制性的,并且已经打算在将来从 MySQL 转移到另一个 DBMS。
Postgres SQL 支持特殊的列类型 uuid
如果定义了 UUID 列,Laravel 将使用它。这不仅将 ID 存储和比较为二进制,而且还检查格式和唯一值,这意味着您不能像在 MySQL 常规 sting 列中那样意外输入无效的 UUID,除了not null
和 unique
.
Laravel: 7; PHP: 7.4/8.0
在我的项目中,我有两个相关模型:User
和 TimeAccount
。两者都使用 UUID 作为主键,键名仍然是 id
.
迁移:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary();
// snip...
});
}
// snip...
}
class CreateTimeAccountsTable extends Migration
{
public function up()
{
Schema::create('time_accounts', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('user_id')->nullable()->constrained();
// snip...
});
}
// snip...
}
型号:
class User
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function timeAccount()
{
return $this->hasOne(TimeAccount::class);
}
// snip...
}
class TimeAccount
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function user()
{
return $this->belongsTo(User::class);
}
// snip...
}
IsIdentifiedByUuid 特征:
trait IsIdentifiedByUuid
{
protected static function bootIsIdentifiedByUuid()
{
static::creating(fn ($model) => static::isIdentifiedByUuidCreatingHandler($model));
static::saving(fn ($model) => static::isIdentifiedByUuidSavingHandler($model));
}
public function initializeIsIdentifiedByUuid()
{
$this->keyType = 'string';
}
protected function getUuidColumn(): string
{
return property_exists($this, 'uuid_column') ? $this->uuid_column : 'id';
}
protected static function getNewUuid(): UuidInterface
{
$columnName = app(static::class)->getUuidColumn();
$tableName = app(static::class)->getTable();
$columnDescriptor = "$tableName.$columnName";
$uuid = "";
$query = \DB::table($tableName)
->select($columnDescriptor)
->where($columnDescriptor, $uuid);
$attempts = 0;
do {
if ($attempts >= \App\Constants\Uuid::MAX_ATTEMPTS) {
throw new UuidMaxGeneratorAttemptsExceededException();
}
$uuid = Str::uuid();
$attempts++;
} while ($query->setBindings([ $uuid->toString() ])->count() > 0);
return $uuid;
}
/**
* Handles the creation of a model.
* - Generates new UUID if the UUID column is empty and auto-increment is enabled
*
* @param Model $model
* @throws \App\Exceptions\UuidMaxGeneratorAttemptsExceededException
*/
protected static function isIdentifiedByUuidCreatingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
if ($model->getIncrementing() && !$model->{$columnName}) {
$uuid = static::getNewUuid();
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Generating new UUID for `" . get_class($model) . ": $uuid`"
);
$model->{$columnName} = $uuid->toString();
} else {
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Using existing UUID for `" . get_class($model) . ": $model->{$columnName}`"
);
}
}
/**
* Handles the saving of a Model.
* - Prevents changes to the UUID column
* - Rolls back changed value to old value
*
* @param Model $model
*/
protected static function isIdentifiedByUuidSavingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
$originalUuid = $model->getOriginal($columnName);
if (!is_null($originalUuid) &&
$originalUuid !== $model->{$columnName}
) {
\Log::debug(
"IsIdentifiedByUuid [SAVING]:" .
" Prevented change of UUID for `" . get_class($model) . ":$originalUuid`"
);
$model->{$columnName} = $originalUuid;
}
}
}
现在的问题是:
在创建的挂钩上的用户观察者中,我正在这样做:
$userTimeAccount = $user->timeAccount()->create([
// snip...
]);
到目前为止 - 在切换到 UUID 而不是整数键之前 - 一切正常!据我所知,在保存用户模型之前(显然是在触发 auto-inc 之前)创建相关模型是 eloquent/Laravel 明确允许的(尽管我找不到文档的部分).
切换到 UUID 后,这不再有效,我收到 SQL 错误:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (
laravel_torus
.time_accounts
, CONSTRAINTtime_accounts_user_id_foreign
FOREIGN KEY (user_id
) REFERENCESusers
(id
)) (SQL: insert intotime_accounts
(time_account_type_id
,balance
,borrow
,user_id
,id
,updated_at
,created_at
) values (3, 0, 0, 0, a995807b-b7e1-4af3-96de-7c48187f943d, 2021-07-09 12:54:25, 2021-07-09 12:54:25))
SQL美化:
insert into `time_accounts`
(`time_account_type_id`, `balance`, `borrow`, `user_id`, `id`, `updated_at`, `created_at`)
values
(3, 0, 0, 0, 'bad86e70-7496-4b42-b2a1-5cc3c5a4d06c', '2021-07-09 11:19:17', '2021-07-09 11:19:17');
不知何故 Laravel 试图为外键插入零(整数),这显然不起作用。我什至尝试手动将 keyType
设置为 'string'
但没有效果。
我哪里错了?
编辑: 似乎存在竞争条件。 Observer 的 created 钩子将在 User
模型的 created 钩子之前被触发。这会导致缺少 ID - 使用整数和自动递增时会出现 ID,正如它应该的那样。
TL;DR
I was not able to solve this with MySQL and switched to Postgres SQL.
DONT USE UUID WITH MYSQL unless you are, absolutely sure that: 1. you need to and 2. the performance impact will be negligible!
我真的无法解释这个错误是怎么发生的。有问题的 ID 已经存在(使用调试器查看模型的 $attribnutes
数组),调用 $user->id
时仍然 Laravel 将 return 整数 0
。
在调查问题时,我注意到 Laravel 正在为 UUID 而不是二进制创建字符串列(这是我所期望的)。由于与 integer/binary 比较相比,字符串比较的成本非常高,所以我决定放弃 MySQL。 UUID 对于此项目是强制性的,并且已经打算在将来从 MySQL 转移到另一个 DBMS。
Postgres SQL 支持特殊的列类型 uuid
如果定义了 UUID 列,Laravel 将使用它。这不仅将 ID 存储和比较为二进制,而且还检查格式和唯一值,这意味着您不能像在 MySQL 常规 sting 列中那样意外输入无效的 UUID,除了not null
和 unique
.