Doctrine/Eloquent的软删除(Soft Delete)实现:数据库设计与查询优化
大家好,今天我们来深入探讨 Doctrine/Eloquent 框架下的软删除(Soft Delete)实现,包括数据库设计、代码实现、查询优化以及一些最佳实践。软删除是一种常见的数据管理策略,它允许我们在逻辑上删除数据,而不是物理删除,从而保留数据的历史信息和审计记录,避免误删导致的数据丢失,并支持数据恢复。
1. 软删除的概念与优势
软删除 (Soft Delete) 是一种数据删除方法,并非直接从数据库中物理删除记录,而是通过设置一个特定的标志位(通常是一个 deleted_at 字段)来标记该记录为已删除。
相比于硬删除 (Hard Delete),软删除具有以下优势:
- 数据恢复: 软删除允许轻松恢复已删除的数据,只需将
deleted_at字段设置为 NULL 即可。 - 审计跟踪: 软删除保留了数据的历史记录,方便审计和分析。
- 避免误删: 软删除避免了因误操作导致的数据永久丢失。
- 数据一致性: 软删除可以维护关联数据的一致性,例如,订单数据可以保留已删除的商品信息。
2. 数据库设计
在数据库设计中,实现软删除的关键是添加一个用于标记删除状态的字段。通常,我们使用 deleted_at 字段,其数据类型为 TIMESTAMP 或 DATETIME。当记录被删除时,deleted_at 字段会被设置为当前的日期和时间;当记录未被删除时,deleted_at 字段的值为 NULL。
示例:用户表 (users)
| Column | Type | Nullable | Default | Comment |
|---|---|---|---|---|
| id | BIGINT | NO | Primary Key | |
| name | VARCHAR(255) | NO | 用户名 | |
| VARCHAR(255) | NO | 邮箱 | ||
| password | VARCHAR(255) | NO | 密码 | |
| created_at | TIMESTAMP | YES | 创建时间 | |
| updated_at | TIMESTAMP | YES | 更新时间 | |
| deleted_at | TIMESTAMP | YES | NULL | 删除时间 |
SQL 创建表语句:
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NULL DEFAULT NULL,
updated_at TIMESTAMP NULL DEFAULT NULL,
deleted_at TIMESTAMP NULL DEFAULT NULL
);
3. Eloquent 模型实现
在 Laravel 的 Eloquent 模型中,我们可以使用 SoftDeletes trait 轻松实现软删除功能。
步骤:
- 引入
SoftDeletestrait: 在模型类中引入IlluminateDatabaseEloquentSoftDeletestrait。 - 定义
$dates属性: 在模型类中定义$dates属性,并将deleted_at添加到该属性中。这告诉 Eloquent 将deleted_at字段视为日期类型。
示例:User 模型
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentSoftDeletes;
class User extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['deleted_at'];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
使用方法:
-
删除: 使用
delete()方法执行软删除。$user = User::find(1); $user->delete(); // 软删除,设置 deleted_at 字段 -
查询: 默认情况下,Eloquent 查询将排除软删除的记录。
$users = User::all(); // 只返回 deleted_at 为 NULL 的记录 -
查询包含软删除的记录: 使用
withTrashed()方法可以查询包含软删除的记录。$users = User::withTrashed()->get(); // 返回所有记录,包括 deleted_at 不为 NULL 的记录 -
仅查询软删除的记录: 使用
onlyTrashed()方法可以仅查询软删除的记录。$users = User::onlyTrashed()->get(); // 只返回 deleted_at 不为 NULL 的记录 -
恢复软删除的记录: 使用
restore()方法可以恢复软删除的记录。$user = User::withTrashed()->find(1); $user->restore(); // 恢复软删除,设置 deleted_at 字段为 NULL -
永久删除记录 (Hard Delete): 使用
forceDelete()方法可以永久删除记录。$user = User::withTrashed()->find(1); $user->forceDelete(); // 永久删除,从数据库中移除记录
4. 查询优化
在使用软删除时,我们需要注意查询优化,以避免性能问题。特别是当数据量较大时,不当的查询可能会导致性能瓶颈。
优化策略:
-
索引: 在
deleted_at字段上创建索引。这可以显著提高查询性能,特别是当我们需要查询未删除的记录时。CREATE INDEX idx_deleted_at ON users (deleted_at); -
避免全表扫描: 尽量避免使用
withTrashed()方法进行全表扫描。如果只需要查询部分软删除的记录,可以使用更精确的条件。例如,根据其他字段进行过滤后再使用withTrashed()。 -
使用
whereNull()和whereNotNull(): 使用whereNull('deleted_at')和whereNotNull('deleted_at')代替where('deleted_at', '=', null)和where('deleted_at', '!=', null)。前者通常性能更好,因为数据库优化器可以更好地利用索引。$users = User::whereNull('deleted_at')->get(); // 查找未删除的用户 $trashedUsers = User::whereNotNull('deleted_at')->get(); // 查找已删除的用户 -
分页查询: 对于大量数据的查询,使用分页可以有效地减少单次查询的数据量,从而提高性能。
$users = User::paginate(10); // 每页显示 10 个用户 -
使用 Eager Loading: 如果你的查询涉及到关联模型,使用 Eager Loading 可以避免 N+1 查询问题,从而提高性能。例如,查询用户及其相关的文章:
$users = User::with('articles')->get(); // 预加载文章 -
缓存: 对于不经常变动的数据,可以使用缓存来减少数据库查询次数,从而提高性能。
use IlluminateSupportFacadesCache; $users = Cache::remember('users', 60, function () { return User::all(); }); -
数据库级别的视图 (Views): 对于复杂的查询,可以考虑创建数据库视图。视图可以预先计算好数据,并提供一个简单的接口供应用程序访问。
CREATE VIEW active_users AS SELECT id, name, email FROM users WHERE deleted_at IS NULL;然后在 Laravel 中,你可以像访问普通表一样访问视图。
-
数据库分析工具: 使用数据库提供的分析工具(例如 MySQL 的
EXPLAIN命令)来分析查询的执行计划,找出潜在的性能瓶颈。EXPLAIN SELECT * FROM users WHERE deleted_at IS NULL;
5. 全局 Scopes
Eloquent 的全局 Scopes 允许你为模型添加约束,这些约束会在每次查询模型时自动应用。我们可以使用全局 Scopes 来自动过滤掉软删除的记录。
步骤:
- 创建 Scope 类: 创建一个实现了
IlluminateDatabaseEloquentScope接口的类。 - 实现
apply()方法: 在apply()方法中,添加查询约束。
示例:SoftDeletingScope
<?php
namespace AppScopes;
use IlluminateDatabaseEloquentBuilder;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentScope;
class SoftDeletingScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* @param IlluminateDatabaseEloquentBuilder $builder
* @param IlluminateDatabaseEloquentModel $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->whereNull($model->getQualifiedDeletedAtColumn());
}
}
注册 Scope:
-
在模型中注册: 在模型的
boot()方法中注册全局 Scope。<?php namespace AppModels; use IlluminateDatabaseEloquentFactoriesHasFactory; use IlluminateDatabaseEloquentModel; use IlluminateDatabaseEloquentSoftDeletes; use AppScopesSoftDeletingScope; class User extends Model { use HasFactory, SoftDeletes; // ... /** * The "booted" method of the model. * * @return void */ protected static function booted() { static::addGlobalScope(new SoftDeletingScope); } }
移除 Scope:
如果你需要临时移除全局 Scope,可以使用 withoutGlobalScope() 方法。
$users = User::withoutGlobalScope(SoftDeletingScope::class)->get();
6. 关联关系处理
在使用软删除时,需要特别注意关联关系的处理。以下是一些常见的场景和处理方法:
-
一对一关系: 如果一个模型与另一个模型存在一对一关系,并且你希望在查询父模型时自动排除已软删除的子模型,可以使用
whereDoesntHave()方法。// 查询没有被软删除的用户资料的用户 $users = User::whereDoesntHave('profile', function ($query) { $query->whereNotNull('deleted_at'); })->get(); -
一对多关系: 类似于一对一关系,可以使用
whereDoesntHave()方法来排除已软删除的子模型。// 查询没有被软删除的文章的用户 $users = User::whereDoesntHave('articles', function ($query) { $query->whereNotNull('deleted_at'); })->get(); -
多对多关系: 多对多关系的处理稍微复杂一些,因为涉及到中间表。你需要确保中间表也包含
deleted_at字段,并且在查询时考虑这个字段。// 假设用户和角色是多对多关系,并且 pivot 表 (user_role) 也有 deleted_at 字段 $users = User::whereDoesntHave('roles', function ($query) { $query->wherePivot('deleted_at', '!=', null); })->get();或者,你可以在定义关联关系时,使用
withPivot()方法来指定中间表的deleted_at字段。public function roles() { return $this->belongsToMany(Role::class)->withPivot('deleted_at'); }然后,在查询时可以使用
wherePivot()方法。$users = User::whereDoesntHave('roles', function ($query) { $query->wherePivot('deleted_at', '!=', null); })->get();
7. 事件 (Events)
Eloquent 提供了丰富的事件机制,可以在模型创建、更新、删除等操作前后触发事件。我们可以利用这些事件来执行一些额外的操作,例如:
-
记录删除日志: 在模型被软删除后,记录删除日志,包括删除的用户、删除时间等信息。
<?php namespace AppModels; use IlluminateDatabaseEloquentFactoriesHasFactory; use IlluminateDatabaseEloquentModel; use IlluminateDatabaseEloquentSoftDeletes; class User extends Model { use HasFactory, SoftDeletes; // ... /** * The "booted" method of the model. * * @return void */ protected static function booted() { parent::booted(); static::deleted(function ($user) { // 记录删除日志 Log::info('User deleted: ' . $user->id); }); } } -
清理关联数据: 在模型被永久删除后,清理与其关联的数据。
<?php namespace AppModels; use IlluminateDatabaseEloquentFactoriesHasFactory; use IlluminateDatabaseEloquentModel; use IlluminateDatabaseEloquentSoftDeletes; class User extends Model { use HasFactory, SoftDeletes; // ... /** * The "booted" method of the model. * * @return void */ protected static function booted() { parent::booted(); static::deleting(function ($user) { // 在永久删除用户之前,删除其相关的文章 $user->articles()->delete(); // 软删除 // 或者 // $user->articles()->forceDelete(); // 永久删除 }); } }
8. 最佳实践
- 统一
deleted_at字段命名: 在所有需要软删除的模型中使用统一的deleted_at字段命名,方便维护和管理。 - 使用全局 Scopes 自动过滤软删除的记录: 可以有效避免在每个查询中都手动添加
whereNull('deleted_at')约束。 - 谨慎使用
forceDelete()方法: 永久删除操作不可逆,务必谨慎使用。建议在生产环境中禁用forceDelete()方法,或者添加额外的权限验证。 - 定期清理软删除的数据: 如果软删除的数据量过大,可能会影响查询性能。建议定期清理软删除的数据,例如,将超过一定时间的软删除数据永久删除。
- 考虑使用数据库触发器 (Triggers): 在某些情况下,可以使用数据库触发器来实现一些复杂的软删除逻辑,例如,自动更新关联表的数据。
代码示例总结
本文提供了多个代码示例,覆盖了软删除的各个方面,包括:
- 模型定义: 展示了如何使用
SoftDeletestrait,以及如何定义$dates属性。 - 基本操作: 演示了如何使用
delete(),restore(),withTrashed(),onlyTrashed(),forceDelete()方法。 - 查询优化: 介绍了如何使用
whereNull(),whereNotNull(),paginate(),with()方法。 - 全局 Scopes: 展示了如何创建和注册全局 Scopes。
- 关联关系处理: 演示了如何使用
whereDoesntHave()方法处理关联关系。 - 事件: 介绍了如何使用事件来执行额外的操作。
关于软删除的思考
软删除是一个强大的工具,但并非适用于所有场景。在选择使用软删除时,需要仔细考虑其优缺点,并根据具体的业务需求进行权衡。比如:对于一些敏感数据,可能需要直接硬删除,以确保数据的安全性。另外,需要权衡软删除带来的数据冗余和查询效率的影响。
保证性能,合理应用
通过本文的讲解,相信大家对 Doctrine/Eloquent 的软删除实现有了更深入的了解。掌握了软删除的原理、实现方法和优化策略,可以更好地应用于实际项目中,提高数据管理的效率和安全性。请记住,在实际应用中,需要根据具体的业务场景和数据量,选择合适的策略,以保证性能和数据的完整性。