yii2dev/ar-softdelete

为 Yii2 提供了 ActiveRecord 软删除支持

安装次数: 3,452

依赖者: 0

建议者: 0

安全: 0

星标: 1

关注者: 1

分支: 0

开放性问题: 0

类型:yii2-extension

2.0.0 2022-09-15 20:47 UTC

This package is auto-updated.

Last update: 2024-09-16 01:20:04 UTC


README

Yii2 ActiveRecord 软删除扩展


本扩展提供了 ActiveRecord 软删除的支持。

有关许可证信息,请参阅 LICENSE 文件。

来源: https://github.com/yii2tech/ar-softdelete

安装

安装此扩展的首选方式是通过 composer

运行以下命令之一:

php composer.phar require --prefer-dist yii2dev/ar-softdelete

用法

本扩展提供了所谓“软”删除的 ActiveRecord 支持,这意味着记录不会被从数据库中删除,而是通过一些标志或状态标记为不再活跃。

本扩展提供了 [[\yii2dev\ar\softdelete\SoftDeleteBehavior]] ActiveRecord 行为,以支持 Yii2 中的此类解决方案。您可以通过以下方式将其附加到模型类中

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                'softDeleteAttributeValues' => [
                    'isDeleted' => true
                ],
            ],
        ];
    }
}

“软”删除有两种应用方式

  • 使用 softDelete() 分离的方法
  • 修改常规 delete() 方法

推荐使用 softDelete(),因为它允许标记记录为“已删除”,同时保持常规 delete() 方法不变,这在需要执行“硬”删除时非常有用。例如

<?php

$id = 17;
$item = Item::findOne($id);
$item->softDelete(); // mark record as "deleted"

$item = Item::findOne($id);
var_dump($item->isDeleted); // outputs "true"

$item->delete(); // perform actual deleting of the record
$item = Item::findOne($id);
var_dump($item); // outputs "null"

但是,您可能希望以执行“软”删除而不是实际删除记录的方式来修改常规 ActiveRecord delete() 方法。这在将“软”删除功能应用于现有代码的情况下是一种常见的解决方案。为此功能,您应在行为配置中启用 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$replaceRegularDelete]] 选项

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                'softDeleteAttributeValues' => [
                    'isDeleted' => true
                ],
                'replaceRegularDelete' => true // mutate native `delete()` method
            ],
        ];
    }
}

现在调用 delete() 方法将标记记录为“已删除”,而不是删除它

<?php

$id = 17;
$item = Item::findOne($id);
$item->delete(); // no record removal, mark record as "deleted" instead

$item = Item::findOne($id);
var_dump($item->isDeleted); // outputs "true"

注意! 如果您修改了常规 ActiveRecord delete() 方法,它将无法与 ActiveRecord 事务功能一起工作,例如 [[\yii\db\ActiveRecord::OP_DELETE]] 或 [[\yii\db\ActiveRecord::OP_ALL]] 事务级别的情况

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                'replaceRegularDelete' => true // mutate native `delete()` method
            ],
        ];
    }

    public function transactions()
    {
        return [
            'some' => self::OP_DELETE,
        ];
    }
}

$item = Item::findOne($id);
$item->setScenario('some');
$item->delete(); // nothing happens!

查询“软”删除的记录

显然,为了只找到“已删除”或只找到“活跃”的记录,您应该在您的搜索查询中添加相应的条件

<?php

// returns only not "deleted" records
$notDeletedItems = Item::find()
    ->where(['isDeleted' => false])
    ->all();

// returns "deleted" records
$deletedItems = Item::find()
    ->where(['isDeleted' => true])
    ->all();

然而,您可以使用 [[yii2dev\ar\softdelete\SoftDeleteQueryBehavior]] 来简化此类查询的构建。最简单的方式是在 [[\yii\db\BaseActiveRecord::find()]] 方法上手动附加此行为。例如

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;
use yii2dev\ar\softdelete\SoftDeleteQueryBehavior;

class Item extends ActiveRecord
{
    // ...
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                // ...
            ],
        ];
    }

    /**
     * @return \yii\db\ActiveQuery|SoftDeleteQueryBehavior
     */
    public static function find()
    {
        $query = parent::find();
        $query->attachBehavior('softDelete', SoftDeleteQueryBehavior::class);
        return $query;
    }
}

如果您已经为您的活动记录定义了自定义查询类,您可以将行为附加到那里。例如

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;
use yii2dev\ar\softdelete\SoftDeleteQueryBehavior;

class Item extends ActiveRecord
{
    // ...
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                // ...
            ],
        ];
    }

    /**
     * @return ItemQuery|SoftDeleteQueryBehavior
     */
    public static function find()
    {
        return new ItemQuery(get_called_class());
    }
}

class ItemQuery extends \yii\db\ActiveQuery
{
    public function behaviors()
    {
        return [
            'softDelete' => [
                'class' => SoftDeleteQueryBehavior::class,
            ],
        ];
    }
}

一旦附加,[[yii2dev\ar\softdelete\SoftDeleteQueryBehavior]] 提供了使用“软”删除标准进行记录过滤的命名范围。例如

<?php

// Find all "deleted" records:
$deletedItems = Item::find()->deleted()->all();

// Find all "active" records:
$notDeletedItems = Item::find()->notDeleted()->all();

// find all comments for not "deleted" items:
$comments = Comment::find()
    ->innerJoinWith(['item' => function ($query) {
        $query->notDeleted();
    }])
    ->all();

您可以使用 filterDeleted() 方法轻松创建用于“已删除”记录的列表过滤器

<?php

// Filter records by "soft" deleted criteria:
$items = Item::find()
    ->filterDeleted(Yii::$app->request->get('filter_deleted'))
    ->all();

此方法在空过滤器值上应用 notDeleted() 范围,在正过滤器值上应用 deleted() - 在负(零)值上则不应用任何范围(例如,显示“已删除”和“活跃”的记录)。

注意:[[yii2dev\ar\softdelete\SoftDeleteQueryBehavior]] 已设计为正确处理连接并避免模糊列错误,然而,仍然可能存在它无法正确处理的情况。如果您正在编写涉及多个表且具有“软删除”功能的复杂查询,请准备好手动指定“软删除”条件。

默认情况下,[[yii2dev\ar\softdelete\SoftDeleteQueryBehavior]] 使用 [[yii2dev\ar\softdelete\SoftDeleteBehavior::$softDeleteAttributeValues]] 中的信息为其作用域创建过滤条件。因此,如果您正在使用复杂的逻辑来标记“软删除”记录,可能需要手动配置过滤条件。例如:

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;
use yii2dev\ar\softdelete\SoftDeleteQueryBehavior;

class Item extends ActiveRecord
{
    // ...
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                'softDeleteAttributeValues' => [
                    'statusId' => 'deleted',
                ],
            ],
        ];
    }

    /**
     * @return \yii\db\ActiveQuery|SoftDeleteQueryBehavior
     */
    public static function find()
    {
        $query = parent::find();
        
        $query->attachBehavior('softDelete', [
            'class' => SoftDeleteQueryBehavior::class,
            'deletedCondition' => [
                'statusId' => 'deleted',
            ],
            'notDeletedCondition' => [
                'statusId' => 'active',
            ],
        ]);
        
        return $query;
    }
}

提示:您可以将过滤“未删除”记录的条件应用到 ActiveQuery 作为默认作用域,从而覆盖 find() 方法。同时,请记住,您可以使用 onCondition()where() 方法并使用空条件来重置此类默认作用域。

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;
use yii2dev\ar\softdelete\SoftDeleteQueryBehavior;

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                'softDeleteAttributeValues' => [
                    'isDeleted' => true
                ],
            ],
        ];
    }

    /**
     * @return \yii\db\ActiveQuery|SoftDeleteQueryBehavior
     */
    public static function find()
    {
        $query = parent::find();
        
        $query->attachBehavior('softDelete', SoftDeleteQueryBehavior::class);
        
        return $query->notDeleted();
    }
}

$notDeletedItems = Item::find()->all(); // returns only not "deleted" records

$allItems = Item::find()
    ->onCondition([]) // resets "not deleted" scope for relational databases
    ->all(); // returns all records

$allItems = Item::find()
    ->where([]) // resets "not deleted" scope for NOSQL databases
    ->all(); // returns all records

智能删除

通常,“软删除”功能用于防止数据库历史记录丢失,确保正在使用的数据以及可能具有引用或依赖的数据保持在系统中。然而,有时也可能允许删除此类数据。例如:通常用户账户记录不应该被删除,而应该仅标记为“不活跃”,然而,如果您浏览用户列表并发现很久以前注册但至少未在系统中进行过一次登录的账户,这些记录对历史记录没有价值,可以从数据库中删除以节省磁盘空间。

您可以通过 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$allowDeleteCallback]] 来使“软删除”变得“智能”,并检测记录是否可以从数据库中删除或仅标记为“已删除”。这可以通过以下方式完成:

<?php
 
use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;

class User extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                'softDeleteAttributeValues' => [
                    'isDeleted' => true
                ],
                'allowDeleteCallback' => function ($user) {
                    return $user->lastLoginDate === null; // allow delete user, if he has never logged in
                }
            ],
        ];
    }
}

$user = User::find()->where(['lastLoginDate' => null])->limit(1)->one();
$user->softDelete(); // removes the record!!!

$user = User::find()->where(['not' =>['lastLoginDate' => null]])->limit(1)->one();
$user->softDelete(); // marks record as "deleted"

如果 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$replaceRegularDelete]] 一起启用,[[\yii2dev\ar\softdelete\SoftDeleteBehavior::$allowDeleteCallback]] 逻辑将被应用。

处理外键约束

在使用支持外键的关系型数据库(如 MySQL、PostgreSQL 等)时,“软删除”广泛用于保持外键一致性。例如:如果用户在在线商店进行购买,有关此购买的信息应该保留在系统中以供未来的账目管理。此类数据结构的 DDL 可能如下所示:

CREATE TABLE `Customer`
(
   `id` integer NOT NULL AUTO_INCREMENT,
   `name` varchar(64) NOT NULL,
   `address` varchar(64) NOT NULL,
   `phone` varchar(20) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE InnoDB;

CREATE TABLE `Purchase`
(
   `id` integer NOT NULL AUTO_INCREMENT,
   `customerId` integer NOT NULL,
   `itemId` integer NOT NULL,
   `amount` integer NOT NULL,
    PRIMARY KEY (`id`)
    FOREIGN KEY (`customerId`) REFERENCES `Customer` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
    FOREIGN KEY (`itemId`) REFERENCES `Item` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE,
) ENGINE InnoDB;

因此,当从 'purchase' 到 'user' 设置外键时,使用的是 'ON DELETE RESTRICT' 模式。因此,当尝试删除至少有一个购买记录的用户记录时,将发生数据库错误。然而,如果用户记录没有外部引用,则可以删除。

对于此类用例,使用 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$allowDeleteCallback]] 不是很实际。它将需要执行额外的查询以确定是否存在外部引用,从而消除外键数据库功能的优势。

方法 [[yii2dev\ar\softdelete\SoftDeleteBehavior::safeDelete()]] 尝试调用常规 [[\yii\db\BaseActiveRecord::delete()]] 方法,如果失败则抛出异常,回退到 [[yii2dev\ar\softdelete\SoftDeleteBehavior::softDelete()]]。

<?php

// if there is a foreign key reference :
$customer = Customer::findOne(15);
var_dump(count($customer->purchases)); // outputs; "1"
$customer->safeDelete(); // performs "soft" delete!
var_dump($customer->isDeleted) // outputs: "true"

// if there is NO foreign key reference :
$customer = Customer::findOne(53);
var_dump(count($customer->purchases)); // outputs; "0"
$customer->safeDelete(); // performs actual delete!
$customer = Customer::findOne(53);
var_dump($customer); // outputs: "null"

默认情况下,safeDelete() 方法捕获 [[\yii\db\IntegrityException]] 异常,这意味着在违反外键约束的数据库异常上执行软删除。您可以在此处指定另一个异常类以自定义回退错误级别。例如:使用 [[\Throwable]] 将在常规删除过程中发生任何错误时触发软删除回退。

记录恢复

在某个时候,您可能希望“恢复”以前标记为“已删除”的记录。您可以使用 restore() 方法来完成此操作。

<?php

$id = 17;
$item = Item::findOne($id);
$item->softDelete(); // mark record as "deleted"

$item = Item::findOne($id);
$item->restore(); // restore record
var_dump($item->isDeleted); // outputs "false"

默认情况下,用于记录恢复的属性值将自动从 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$softDeleteAttributeValues]] 中检测,但最好您通过 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$restoreAttributeValues]] 显式指定它们。

提示:如果您启用 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::$useRestoreAttributeValuesAsDefaults]],则标记已恢复记录的属性值将自动应用于新记录的插入。

事件

默认情况下,[[\yii2dev\ar\softdelete\SoftDeleteBehavior::softDelete()]] 以与常规 delete() 相同的方式触发 [[\yii\db\BaseActiveRecord::EVENT_BEFORE_DELETE]] 和 [[\yii\db\BaseActiveRecord::EVENT_AFTER_DELETE]] 事件。

此外 [[\yii2dev\ar\softdelete\SoftDeleteBehavior]] 还会在拥有者的 ActiveRecord 范围内触发几个附加事件

  • [[\yii2dev\ar\softdelete\SoftDeleteBehavior::EVENT_BEFORE_SOFT_DELETE]] - 在执行“软删除”之前触发。
  • [[\yii2dev\ar\softdelete\SoftDeleteBehavior::EVENT_AFTER_SOFT_DELETE]] - 在执行“软删除”之后触发。
  • [[\yii2dev\ar\softdelete\SoftDeleteBehavior::EVENT_BEFORE_RESTORE]] - 在从“已删除”状态恢复记录之前触发。
  • [[\yii2dev\ar\softdelete\SoftDeleteBehavior::EVENT_AFTER_RESTORE]] - 在从“已删除”状态恢复记录之后触发。

您可以将这些事件的处理器附加到您的 ActiveRecord 对象上

<?php

$item = Item::findOne($id);
$item->on(SoftDeleteBehavior::EVENT_BEFORE_SOFT_DELETE, function($event) {
    $event->isValid = false; // prevent "soft" delete to be performed
});

您还可以通过声明相应的方法来在您的 ActiveRecord 类内部处理这些事件

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::class,
                // ...
            ],
        ];
    }

    public function beforeSoftDelete()
    {
        $this->deletedAt = time(); // log the deletion date
        return true;
    }

    public function beforeRestore()
    {
        return $this->deletedAt > (time() - 3600); // allow restoration only for the records, being deleted during last hour
    }
}

事务操作

您可以将 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::softDelete()]] 方法的调用显式地包含在一个事务块中,如下所示

<?php

$item = Item::findOne($id);

$transaction = $item->getDb()->beginTransaction();
try {
    $item->softDelete();
    // ...other DB operations...
    $transaction->commit();
} catch (\Exception $e) { // PHP < 7.0
    $transaction->rollBack();
    throw $e;
} catch (\Throwable $e) { // PHP >= 7.0
    $transaction->rollBack();
    throw $e;
}

或者您可以使用 [[\yii\db\ActiveRecord::transactions()]] 方法来指定应在事务块中执行的操作列表。方法 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::softDelete()]] 同时响应 [[\yii\db\ActiveRecord::OP_UPDATE]] 和 [[\yii\db\ActiveRecord::OP_DELETE]]。如果当前模型场景至少包含这些常量之一,软删除将在事务块内执行。

注意:方法 [[\yii2dev\ar\softdelete\SoftDeleteBehavior::safeDelete()]] 使用其自己的内部事务逻辑,这可能与自动事务操作冲突。请确保您不会在受 [[\yii\db\ActiveRecord::transactions()]] 影响的场景中运行此方法。

乐观锁

软删除支持乐观锁,方式与常规 [[\yii\db\ActiveRecord::save()]] 方法相同。如果您通过 [[\yii\db\ActiveRecord::optimisticLock()]] 指定了版本属性,[[\yii2dev\ar\softdelete\SoftDeleteBehavior::softDelete()]] 将在版本号过时时抛出 [[\yii\db\StaleObjectException]] 异常。例如,如果您将 ActiveRecord 定义如下

<?php

use yii\db\ActiveRecord;
use yii2dev\ar\softdelete\SoftDeleteBehavior;

class Item extends ActiveRecord
{
    /**
     * {@inheritdoc}
     */
    public function behaviors()
    {
        return [
            'softDelete' => [
                'class' => SoftDeleteBehavior::class,
                'softDeleteAttributeValues' => [
                    'isDeleted' => true
                ],
            ],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function optimisticLock()
    {
        return 'version';
    }
}

您可以通过以下方式创建删除链接

<?php
use yii\helpers\Html;

/* @var $model Item */
?>
...
<?= Html::a('delete', ['delete', 'id' => $model->id, 'version' => $model->version], ['data-method' => 'post']) ?>
...

然后您可以在控制器动作代码中捕获 [[\yii\db\StaleObjectException]] 异常来解决问题

<?php

use yii\db\StaleObjectException;
use yii\web\Controller;

class ItemController extends Controller
{
    public function delete($id, $version)
    {
        $model = $this->findModel($id);
        $model->version = $version;
        
        try {
            $model->softDelete();
            return $this->redirect(['index']);
        } catch (StaleObjectException $e) {
            // logic to resolve the conflict
        }
    }
    
    // ...
}