yii2tech / ar-softdelete
为 Yii2 提供了 ActiveRecord 软删除支持
Requires
- yiisoft/yii2: ~2.0.13
Requires (Dev)
- phpunit/phpunit: 4.8.27|^5.0|^6.0
README
为 Yii2 的 ActiveRecord 软删除扩展
此扩展为 ActiveRecord 软删除提供支持。
有关许可证信息,请查看 LICENSE 文件。
安装
安装此扩展的首选方式是通过 composer。
运行以下命令之一:
php composer.phar require --prefer-dist yii2tech/ar-softdelete
或者将以下内容添加到你的 composer.json 的 require 部分:
"yii2tech/ar-softdelete": "*"
使用方法
此扩展为所谓的“软”删除 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 ], ], ]; } }
有两种“软”删除的应用方式
- 使用
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]] 还在拥有 Active Record 的范围内触发几个附加事件
- [[\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]] - 在从“已删除”状态恢复记录之后触发。
您可以将这些事件的处理器附加到您的 Active Record 对象上。
<?php $item = Item::findOne($id); $item->on(SoftDeleteBehavior::EVENT_BEFORE_SOFT_DELETE, function($event) { $event->isValid = false; // prevent "soft" delete to be performed });
您还可以通过声明相应的方法在 Active Record 类内部处理这些事件。
<?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]] 异常。例如,如果您的 Active Record 定义如下:
<?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 } } // ... }