具有单表继承和不同可连接(STI)的活动存储#43780



我在构建的解决方案中使用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查询。

我发现的最简单、性能最好的解决方案是声明派生的ContentblockContentBlock类本身上可能具有的所有关系。

然后,我在每个单独的块上声明了优化的默认作用域。

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#imageProductBlock#product_image

最新更新