Rails 6.0 ActiveRecord软删除default_scope覆盖STI查询问题
问题根因
故障核心出在原有软删除concern的两处实现:
not_deletedscope里调用了base.unscoped,这个方法会清空当前查询链上所有已挂载的默认条件,包括Rails为STI模型自动追加的type过滤规则,不会走官方文档说明的default_scope AND合并逻辑——因为unscoped直接把前置的scope全清掉了。- default_scope硬绑定了
base(也就是include concern的类)的上下文,STI子类继承时不会动态适配子类的查询上下文,后续手动加type scope时也因为上下文提前绑定,导致type值固定为基类名。
方案1:完全不修改原有软删除concern的修复方式
直接在STI基类中重写查询构造的底层入口relation方法,动态追加type过滤条件即可,完全不需要改动concern代码,其他使用该concern的存量非STI模型不受任何影响:
class Widget < ApplicationRecord include ActiveRecordSoftDeletion # 所有查询构造都会走该方法,动态匹配当前调用类的STI类型 def self.relation super.where(type: sti_name) end end
实现逻辑:
- 不管default_scope里的unscoped怎么清空条件,最终生成查询时都会经过
relation方法,这里追加的条件不会被清掉 - 方法内的
self是实际发起查询的类:Thingy.first调用时self是Thingy,自动追加type = 'Thingy';Dohicky.first调用时self是Dohicky,自动追加type = 'Dohicky';基类Widget.first调用时追加type = 'Widget',完全符合STI的预期行为。
方案2:修改软删除concern的兼容实现(零影响存量模型)
如果允许调整concern代码,可以从根源修复STI兼容问题,且所有存量非STI模型的行为和之前完全一致,不需要做任何业务代码改动。
修改后的concern代码如下:
module ActiveRecordSoftDeletion extend ActiveSupport::Concern included do scope :not_deleted, -> { unscope(where: :deleted).where(deleted: false) } scope :only_deleted, -> { unscope(where: :deleted).where(deleted: true) } default_scope { not_deleted } def destroy self.deleted = true self.save(validate: false) end def recover self.deleted = false self.save(validate: false) end end end
调整点说明:
- 把原来scope里的
base.unscoped替换为unscope(where: :deleted):原来的unscoped会清空所有查询条件(包括STI的type、其他模型自定义的default_scope),改成只清除deleted字段的过滤条件,既避免了链式调用时deleted条件重复叠加,又不会误删其他必要的默认条件。 - 去掉default_scope和scope定义里硬编码的
base.前缀:scope块执行时的self会自动指向当前发起查询的类(STI场景下就是子类),Rails会自动追加STI要求的type过滤条件,不会出现type值固定为基类的问题。 - 实例方法
destroy、recover逻辑完全保留,存量业务的删除、恢复调用不需要做任何修改。
内容的提问来源于stack exchange,提问作者Doug Couvillion
相关产品推荐
相关产品推荐

