dfware/ar-softdelete

为Yii2提供ActiveRecord软删除支持

资助包维护!
klimov-paul
Patreon

安装: 50

依赖: 0

建议: 0

安全: 0

星标: 0

关注者: 0

分支: 47

开放问题: 0

类型:yii2-extension

1.0.0 2023-09-27 17:48 UTC

This package is auto-updated.

Last update: 2024-09-27 20:06:09 UTC


README

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

可以运行

composer require dfware/ar-softdelete

或者添加

"dfware/ar-softdelete": "*"

到你的composer.json文件的要求部分。

使用方法

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

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

<?php

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

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

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

  • 使用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()方法。这在将“软删除”功能应用于现有代码的情况下是一种常见的解决方案。为此功能,您应在行为配置中启用[\yii2tech\ar\softdelete\SoftDeleteBehavior::$replaceRegularDelete]选项

<?php

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

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::className(),
                '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 yii2tech\ar\softdelete\SoftDeleteBehavior;

class Item extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::className(),
                '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();

但是,您可以使用[\yii2tech\ar\softdelete\SoftDeleteQueryBehavior]来简化此类查询的编写。应用此行为的最简单方法是在[\yii\db\BaseActiveRecord::find()]方法中手动将其附加到查询实例。例如

<?php

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

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

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

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

<?php

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

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

    /**
     * @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::className(),
            ],
        ];
    }
}

一旦附加[[yii2tech\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(),在负(零)值上不应用范围(例如,显示“已删除”和“活跃”记录)。

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

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

<?php

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

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

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

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

<?php

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

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

    /**
     * @return \yii\db\ActiveQuery|SoftDeleteQueryBehavior
     */
    public static function find()
    {
        $query = parent::find();
        
        $query->attachBehavior('softDelete', SoftDeleteQueryBehavior::className());
        
        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

智能删除

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

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

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

class User extends ActiveRecord
{
    public function behaviors()
    {
        return [
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::className(),
                '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"

当 [[\yii2tech\ar\softdelete\SoftDeleteBehavior::$replaceRegularDelete]] 也被启用时,会应用 [[\yii2tech\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' 模式。因此,当尝试删除至少有一条购买记录的用户记录时,将发生数据库错误。然而,如果用户记录没有外部引用,则可以删除。

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

方法 [[yii2tech\ar\softdelete\SoftDeleteBehavior::safeDelete()]] 尝试调用常规 [[\yii\db\BaseActiveRecord::delete()]] 方法,如果失败并抛出异常,则回退到 [[yii2tech\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"

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

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

事件

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

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

  • [[\yii2tech\ar\softdelete\SoftDeleteBehavior::EVENT_BEFORE_SOFT_DELETE]] - 在执行“软删除”之前触发。
  • [[\yii2tech\ar\softdelete\SoftDeleteBehavior::EVENT_AFTER_SOFT_DELETE]] - 在执行“软删除”之后触发。
  • [[\yii2tech\ar\softdelete\SoftDeleteBehavior::EVENT_BEFORE_RESTORE]] - 在从“已删除”状态恢复记录之前触发。
  • [[\yii2tech\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 yii2tech\ar\softdelete\SoftDeleteBehavior;

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

    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
    }
}

事务操作

您可以将[[\yii2tech\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()]]方法指定应在事务块内执行的操作列表。方法[[\yii2tech\ar\softdelete\SoftDeleteBehavior::softDelete()]]对[[\yii\db\ActiveRecord::OP_UPDATE]]和[[\yii\db\ActiveRecord::OP_DELETE]]都做出响应。如果当前模型场景包括至少其中一个常量,则软删除将在事务块内执行。

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

乐观锁

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

<?php

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

class Item extends ActiveRecord
{
    /**
     * {@inheritdoc}
     */
    public function behaviors()
    {
        return [
            'softDelete' => [
                'class' => SoftDeleteBehavior::className(),
                '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
        }
    }
    
    // ...
}