tranzakt/laravel-softdeletesunique

允许正确唯一索引null softDelete deleted_at列的包。

v1.0-BETA2 2022-12-04 14:17 UTC

This package is auto-updated.

Last update: 2024-09-05 02:51:51 UTC


README

此扩展的目的是在使用软删除的表时使SQL唯一约束(唯一索引)正常工作。

假设您有一个包含id和name字段的模型 - 您希望name是唯一的,因此您在"name"字段上创建一个唯一索引。如果您为'Pete'创建了一个记录然后(硬)删除它,那么您当然可以创建一个新的名为'Pete'的记录,而不会重复名称。

但是,您随后决定使用软删除,它添加了一个deleted_at字段,对于未删除的记录,该字段为null,对于已删除的记录,该字段包含时间戳。

当然,想法是带有软删除的模型的行为与不带它时相同,除了可以恢复已删除的记录。但是,如果您为'Pete'创建了一个记录然后软删除它,那么您希望模型的行为与您硬删除它时相同,并且仍然允许您创建一个名为'Pete'的新(未删除)记录,与已删除的版本并列。因此,您希望deleted_atname的组合是唯一的,所以您在['deleted_at', 'name']上创建一个唯一索引,期望它防止重复,即在您的迁移中,您替换...

$table->string('email')->unique();

$table->string('email');
$table->softDeletes();
$table->unique(['deleted_at', 'email']);

但是这里有一个陷阱在等着您!(您可能不会明确测试这一点,但它将是一个即将发生的问题)。

不幸的是,大多数(但并非所有)SQL RDBMS都遵循SQL标准,该标准将每个NULL值定义为与其他所有NULL值不同。是的,NULL != NULL(这不是一个打字错误!!),这意味着唯一索引奇怪地允许您有多个条目[NULL, 'Pete']!!!,这意味着唯一索引不能防止添加重复记录。

这就是这个包要解决的问题。

它是通过创建一个新的列deleted_at_uniqueable来实现的,该列作为deleted_at列的字符串版本来维护;如果deleted_at列是null,则使用空字符串''

您的代码现在需要如下所示

$table->string('email');
$table->softDeletes();
$table->softDeletesUnique();
$table->unique(['deleted_at_uniqueable', 'email']);

限制

Eloquent-only

softDeletes是一个Eloquent函数且仅在您使用Eloquent而不是使用QueryBuilder或原始DB时才工作的方式一样,此包仅在您使用Eloquent时维护deleted_at_uniqueable列(尽管如果您像维护deleted_at列那样手动维护此列,则将强制执行SQL唯一性)。

数据库维护的唯一性 vs. 验证

此包在SQL数据库级别阻止插入重复的未删除记录。如果您尝试插入重复记录,将抛出一个Illuminate\Database\QueryException(您当然可以捕获它,如果您愿意的话)。

对于创建或更新记录的用户请求,大多数开发者可能会希望确保记录在请求验证阶段将是唯一的;此包可以用作此类验证的替代品或补充。

在恢复垃圾记录或对于不是直接由用户发起的请求时,开发者需要实施手动检查以确保结果将是唯一的,或者使用此包并捕获任何产生的QueryExceptions

要验证上述示例中的用户请求,您通常会在您的请求中具有以下验证

在软删除之前

    public function rules()
    {
        return [
            'email'=>'required|unique:users'
        ];
    }

使用软删除

    public function rules()
    {
        return [
            'email'=>[
                'required',
                Rule:unique('users')->ignore($user)
                    ->where(fn ($query) => $query->whereNull('deleted_at'))
            ]
        ];
    }

使用软删除和软删除唯一

    public function rules()
    {
        return [
            'email'=>[
                'required',
                Rule:unique('users')->ignore($user)
                    ->where(fn ($query) => $query->where('deleted_at_uniqueable', ''))
            ]
        ];
    }

选择性能

当您使用SoftDeletes模型选择(未删除的)记录时,即您不使用withTrash()onlyTrash()修饰符,Laravel的Eloquent会自动将WHERE deleted_at IS NULL添加到SELECT查询中。在底层,对唯一性进行请求验证测试可能会生成类似这样的选择语句。对于这类查询,为了使数据库优化器避免全表扫描,您可能仍然需要在deleted_at上创建某种类型的索引。由于我们现在使用deleted_at_uniqueable进行唯一索引,您可能还需要在deleted_at字段上创建一个非唯一索引,即您的迁移需要看起来...

$table->string('email');
$table->softDeletes();
$table->softDeletesUnique();
$table->index(['deleted_at', 'email']);
$table->unique(['deleted_at_uniqueable', 'email']);

安装 & 使用

安装

composer require Tranzakt/Laravel-SoftDeletesUnique

安装完成后,softDeletesUnique支持会自动添加到迁移蓝图对象中。

使用

在您的迁移...

  1. 添加$table->softDeleteUnique()->after('deleted_at');
  2. $table->unique(['deleted_at', 'column']);替换为$table->unique(['deleted_at_uniqueable', 'column']);
  3. 使用$table->softDeletes()->index();$table->index('deleted_at');deleted_at上添加一个非唯一索引。
public function up()
{
    Schema::create('table_name', function (Blueprint $table) {
        ...

        $table->string('email');
        $table->softDeletes()->index();
        $table->softDeletesUnique();
        $table->unique(['deleted_at_uniqueable', 'email']);
    });
}

在您的模型...

  1. 在头部添加use Tranzakt\softDeletesUnique\Concerns\HasSoftDeletesUnique;,并在类的顶部添加use HasSoftDeletesUnique;
use Tranzakt\softDeletesUnique\Concerns\HasSoftDeletesUnique;

class TableName extends Model {
    use HasSoftDeletesUnique;
}

像往常一样,您可以在softDeletesUnique('deleted_at_str')上使用参数来创建具有不同名称的列,并在您的模型中使用CONST DELETED_AT_UNIQUEABLE = 'deleted_at_str';来告诉模型该列的名称。

工作原理

此包已编写,以尽可能全面地使用标准的Laravel Eloquent功能。

softDeletesUniquedropSoftDeletesUnique方法被宏到蓝图。

$table->softDeletesUnique();创建一个新非空字符串列deleted_at_uniqueable,长度最多为24个字符(格式为'YYYY-MM-DD HH:MM:SS.xxxxxx'),当deleted_at为null时包含'',否则包含其字符串表示。

HasSoftDeletesUnique特性会在创建、更新、删除和恢复Eloquent操作时创建观察者,并确保将deleted_at_uniqueable列设置得适当。

就是这样。

替代方案

此包只是解决数据库唯一约束问题的一种方法,但它被认为是唯一一种适用于所有Laravel支持的RDBMS而无需更改的常见方法,并且不需要在迁移中进行任何特殊的DB:raw命令。

然而,根据您使用的RDBMS,有其他解决方案(包括必要时添加一个索引来支持softDeletes添加的WHERE deleted_at IS NULL

PostgreSQL / SQLite

使用以下两个部分(过滤)索引

CREATE UNIQUE INDEX active_email_unique ON MyTable (`email`) WHERE `deleted_at` IS NULL;
CREATE UNIQUE INDEX deleted_email_unique ON MyTable (`deleted_at`, `email`) WHERE `deleted_at` IS NOT NULL;

Laravel模式对象不包含在索引上定义WHERE子句的能力,因此您需要使用DB::raw来创建和执行上述SQL数据定义语句。

由于当deleted_at为NULL和NOT NULL时我们有单独的部分索引,数据库应该能够使用这些索引之一,当Eloquent的软删除功能在选择语句中添加WHERE deleted_at IS NULL时。

Microsoft SQL Server

Microsoft SQL Server认为NULL===NULL,因此不需要特殊处理。

MySQL / MariaDB

不幸的是,MySQL和MariaDB都不支持带有WHERE子句的索引,我们需要使用“虚拟列”。

创建虚拟列和索引所需的原始SQL如下所示

ALTER TABLE MyTable
ADD COLUMN deleted_at_unique VARCHAR(19) GENERATED ALWAYS AS
IF(`deleted_at` IS NULL, '-', `deleted_at`) VIRTUAL;
CREATE UNIQUE INDEX email_unique_index ON MyTable(`deleted_at_unique`, `email`);

我没有测试这个,但是我怀疑这个索引是否会用于WHERE deleted_at IS NULL子句,因此可能还需要为性能(如果有其他列,这个索引会更有用)添加一个非唯一索引在deleted_at上。

上述Laravel代码如下所示

$table->string('deleted_at_unique')->virtualAs('IF(`deleted_at` IS NULL, '-', `deleted_at`)');
$table->unique('deleted_at_unique', 'email');

许可证

此包受MIT开源许可证的许可。

致谢

本软件包的构建是站在那些已经完成了识别问题和解决方案艰巨工作的前辈的肩膀之上的。

本软件包最初由Sophist编写,并有以下人士的额外贡献:

如果你提交了PR,请在你的PR中添加你的名字到上面的列表中。