FastAdmin框架中关联查询软删除失效的解决方案
2026/9/12 4:01:00 网站建设 项目流程

1. 问题现象与背景分析

最近在FastAdmin框架(基于ThinkPHP5)开发时遇到一个典型问题:在进行关联模型查询时,软删除(soft delete)功能意外失效。具体表现为使用with()join()关联查询时,即使主表数据已被软删除,仍然能被查询出来。

这种情况在管理后台开发中尤为常见。比如我们有个文章表(article)和分类表(category),文章表设置了软删除字段delete_time,但当通过分类关联查询文章时,已被删除的文章仍然会出现在结果集中。

2. 软删除机制原理解析

2.1 ThinkPHP5的软删除实现

ThinkPHP5通过SoftDeletetrait实现软删除功能,核心逻辑是:

  1. 数据表需要delete_time字段(默认名,可配置)
  2. 删除操作变为更新操作,将delete_time设为当前时间
  3. 查询时自动加上delete_time IS NULL条件
// 典型用法 use traits\model\SoftDelete; class Article extends Model { use SoftDelete; protected $deleteTime = 'delete_time'; }

2.2 关联查询的特殊性

当进行关联查询时,ThinkPHP5会生成类似这样的SQL:

SELECT * FROM category LEFT JOIN article ON category.id = article.category_id WHERE category.id = 1

问题在于:软删除的自动条件article.delete_time IS NULL没有被自动加上。

3. 解决方案深度剖析

3.1 方案一:全局设置关联软删除(推荐)

修改关联模型定义,显式声明需要应用软删除:

// Category模型中 public function articles() { return $this->hasMany('Article')->withTrashed(false); }

关键点:

  • withTrashed(false)表示关联查询也应用软删除规则
  • 需要在所有关联定义中添加此设置
  • FastAdmin中建议在对应的模型文件中修改

3.2 方案二:查询时临时设置

在具体查询时动态指定:

Category::with(['articles' => function($query){ $query->where('article.delete_time', null); }])->select();

优势:

  • 灵活性高,可针对不同场景设置
  • 不影响其他关联查询

3.3 方案三:修改底层逻辑(高级)

继承修改Query类:

// 新建extend/traits/SoftDelete.php trait SoftDelete { protected function base($query) { if ($query->getOptions('with')) { foreach ($query->getOptions('with') as $relation) { $this->checkRelationSoftDelete($query, $relation); } } return parent::base($query); } }

注意:此方案需要较强的框架理解能力,不建议新手直接使用

4. FastAdmin环境下的特殊处理

4.1 后台列表关联查询

FastAdmin的CRUD控制器中,修改index方法:

protected function indexBuilder() { return $this->model ->with(['articles' => function($query){ $query->whereNull('delete_time'); }]) ->where($this->getWhere()); }

4.2 表格渲染适配

修改对应的index.html模板:

{foreach $row.articles as $article} {if !$article.delete_time} <!-- 正常显示内容 --> {/if} {/foreach}

5. 常见问题排查指南

5.1 检查清单

现象可能原因解决方案
关联查询返回已删除数据未设置withTrashed方案一或方案二
部分关联数据缺失误用withTrashed(true)检查关联定义
分页总数不正确统计时未过滤软删除使用fetchSql调试

5.2 调试技巧

  1. 获取最终SQL:
echo $model->fetchSql(true)->with('articles')->find();
  1. 检查模型继承链:
print_r(class_parents($model));
  1. 验证软删除字段:
dump($model->getDeleteTimeField());

6. 性能优化建议

  1. 索引优化:
ALTER TABLE `article` ADD INDEX `idx_category_delete` (`category_id`, `delete_time`);
  1. 关联查询替代方案:
// 代替with,使用join+where组合 $list = Category::alias('c') ->join('article a', 'c.id = a.category_id AND a.delete_time IS NULL') ->select();
  1. 缓存策略:
$result = Cache::remember('category_list', function(){ return Category::with(['articles' => function($query){ $query->whereNull('delete_time'); }])->select(); }, 3600);

7. 扩展应用场景

7.1 多层级关联处理

当存在多层关联时(如分类→文章→评论),需要逐级设置:

Category::with(['articles.comments' => function($query){ $query->whereNull('delete_time'); }])->select();

7.2 动态软删除条件

根据不同业务场景动态调整:

$withDeleted = input('param.show_deleted'); Article::with(['comments' => function($query) use ($withDeleted){ if (!$withDeleted) { $query->whereNull('delete_time'); } }]);

7.3 与其他查询条件组合

复杂查询示例:

Article::with(['user' => function($query){ $query->whereNull('delete_time') ->where('status', 'normal') ->field('id,nickname'); }])->where('create_time', '>', '2023-01-01') ->order('view_count DESC') ->select();

8. 最佳实践总结

  1. 一致性原则:
  • 所有关联模型统一使用软删除
  • 数据库字段命名规范统一(建议都用delete_time)
  1. 代码规范建议:
/** * 关联文章 * @return \think\model\relation\HasMany */ public function articles() { return $this->hasMany('Article') ->whereNull('delete_time') ->field('id,category_id,title'); }
  1. 测试用例设计:
public function testSoftDeleteWithRelation() { $article = Article::find(1); $article->delete(); // 软删除 $category = Category::with('articles')->find(1); $this->assertEmpty($category->articles); // 应返回空数组 }

在实际项目开发中,我发现这个问题最容易在以下场景被忽略:

  • 从简单查询升级到关联查询时
  • 接手他人代码进行功能扩展时
  • 快速开发原型阶段忽略细节时

建议在项目初期就建立完善的模型关联测试套件,特别是对数据状态的测试要全面覆盖软删除场景。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询