我在构建的解决方案中使用ActiveStorage来创建具有类似块生成器接口的文档,但在将STI与活动存储和不同附件名称结合使用时,我遇到了N+1查询问题。
给定一个对可内容多态的ContentBlock
class ContentBlock < ApplicationRecord
belongs_to :contentable, polymorphic: true
end
具有这些不同类型的ContentBlock的文档(_M(。
class Document
has_many :content_blocks, -> { order(position: :asc) }, as: :contentable, dependent: :destroy
end
以内容块的子集为例:ImageBlock、ProductBlock,每个都有一个或多个不同的附加文件名称。
class ImageBlock < ContentBlock
has_one_attached :image
end
class ProductBlock < ContentBlock
has_one_attached :product_image
end
通过提供includes(:content_blocks(,查询此文档及其所有关联的ContentBlock记录非常容易。
当需要有关ActiveStorage的信息时,问题就开始了:ContentBlock 的每个子集中都包含附件记录
with_attached_image适用于ImageBlock,但不适用于ProductBlock
with _attached_product_image适用于ProductBlock,但不适用于ImageBlock
我似乎想不出一种方法来加载所有相关的ActiveStorage::Attachment&ActiveStorage::Blob记录,而不会碰到N+1查询。
我处理这个问题的方式不对吗?还有其他方法吗?
解决方案:
如前所述,不可能解决基于内容块的关系类型的N+1查询。
我发现的最简单、性能最好的解决方案是声明派生的Contentblock
在ContentBlock
类本身上可能具有的所有关系。
然后,我在每个单独的块上声明了优化的默认作用域。
class ContentBlock
# Relations are defined here to make proper use of includes(:relation)
# ContentBlock::Image
has_one_attached :image
# ContentBlock::Sign
has_many :signatures, as: :signable, dependent: :destroy
DEFAULT_SCOPE = [
image_attachment: :blob,
signatures: Signature::DEFAULT_SCOPE
].freeze
default_scope do
includes(DEFAULT_SCOPE)
end
end
这样做的缺点是在某些情况下可能会过度蚀刻数据。TextBlock
也将连接到active_storage_attachments
,即使它没有附加图像。
好的一面是,N+1查询已经完全消失了,同时还需要对一些记录进行过度蚀刻,这在我的用例中是非常好的。
您可以尝试将has_one_attached
移动到ContentBlock
class ContentBlock < ApplicationRecord
belongs_to :contentable, polymorphic: true
has_one_attached :media
class << self
def media_alias(name)
alias_method name, :media
alias_method "#{name}=".to_sym, :media=
end
end
end
class ImageBlock < ContentBlock
media_alias :image
end
class ProductBlock < ContentBlock
media_alias :product_image
end
现在,您可以使用with_attached_media来预加载附件,并且仍然可以像以前一样访问ImageBlock#image
、ProductBlock#product_image
。