Rails 5.2:在加载多个带有调整大小的附件的模型时避免 N+1

Rails5.2: avoid N+1 when loading multiples models with resized attachments

我有 4 个模型(Album、Samplepack、Demo 和 Ghostproduction)我想在一个视图中显示。其中两个(Album 和 Samplepack)附有封面图片。

这是生成的流程:


控制器加载4个模型:

# app/controllers/static_controller

class StaticController < ApplicationController
  def index
    @resources = [
      Album.all.with_attached_album_artwork,
      Samplepack.all.with_attached_album_artwork,
      Demo.all,
      Ghostproduction.all
    ].flatten
  end
end

显示@resources的视图:

# app/views/static/index.html.erb

<% @resources.sort_by(&:created_at).each do |resources| %>
  <%= display_mosaic(resource) %>
<% end %>

对应帮手:

# app/helpers/application_helper.rb

def display_mosaic(item)
  if item.class == Demo
    return render('demos/hoverable_demo', demo: item)
  elsif item.class == Samplepack
    return render('samplepacks/hoverable_samplepack', samplepack: item)
#  ... and so on
end

部分示例:

# app/views/albums/_hoverable_album.html.erb

<div>
  <%= image_tag album.large if album.album_artwork.attached? %>
</div>

以及 ActiveStorage 变体:

# app/models/concerns/artworkable_concern.rb

def large
  return self.album_artwork.variant(resize: '600x600').processed
end


使用此解决方案,会产生 14 个查询:

# logs

# Album.all.with_attached_album_artwork
  Album Load (0.7ms)  SELECT "albums".* FROM "albums"
  # Album attachments
    ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" =  AND "active_storage_attachments"."name" =  AND "active_storage_attachments"."record_id" IN (, , , )  [["record_type", "Album"], ["name", "album_artwork"], ["record_id", 1], ["record_id", 2], ["record_id", 3], ["record_id", 4]]
    ActiveStorage::Blob Load (0.5ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" IN (, , , )  [["id", 1], ["id", 2], ["id", 3], ["id", 6]]

# Samplepack.all.with_attached_album_artwork
  Samplepack Load (0.4ms)  SELECT "samplepacks".* FROM "samplepacks"
  # Samplepack attachments
    ActiveStorage::Attachment Load (0.2ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" =  AND "active_storage_attachments"."name" =  AND "active_storage_attachments"."record_id" =   [["record_type", "Samplepack"], ["name", "album_artwork"], ["record_id", 1]]
    ActiveStorage::Blob Load (0.1ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" =   [["id", 5]]

# Demo.all
  Demo Load (0.3ms)  SELECT "demos".* FROM "demos"

# Ghostproduction.all
  Ghostproduction Load (0.5ms)  SELECT "ghostproductions".* FROM "ghostproductions"

# User, because of some logic in my _navbar partial
  User Load (0.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" =  ORDER BY "users"."id" ASC LIMIT  

# Variants (.large), I believe it's a N+1 issue (I currently have 4 Album + 1 Samplepack attachments to resize)
  ActiveStorage::Blob Load (2.9ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" =  LIMIT   [["id", 3], ["LIMIT", 1]] 
  ActiveStorage::Blob Load (0.4ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" =  LIMIT   [["id", 1], ["LIMIT", 1]]
  ActiveStorage::Blob Load (21.0ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" =  LIMIT   [["id", 5], ["LIMIT", 1]]
  ActiveStorage::Blob Load (1.7ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" =  LIMIT   [["id", 2], ["LIMIT", 1]]
  ActiveStorage::Blob Load (0.5ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" =  LIMIT   [["id", 6], ["LIMIT", 1]]

有什么方法可以减少查询量并提高此视图的性能吗?我读过有关 Eager Loading 的内容,但它似乎不合适(这些模型之间没有关系)。 作为初学者,将不胜感激。

祝你有美好的一天, 克莱门特


编辑: 我找到了这个 great guide from EvilMartian

EDIT2: Another resource(更详尽)

如果所有东西都有图像附件,那么您可以彻底改变您的想法,直接加载附件,并包含多态记录。附件也是 ActiveRecord 模型。你可能有:

@resources =
  ActiveStorage::Attachment.
    where(record_type: %w[Album Samplepack Demo Production]).
    includes(:record).
    map(&:record)

用于主要数据获取。它不会是一个查询,但 Rails 会将其优化为最小查询数(一个用于所有附件,一个用于加载多态集合中的每种类型)。

对于变体,如果不对 Rails 打补丁,blob 查询可能无法停止。将自己束缚在 N+1 查询中的另一种方法是缓存,实际上 DHH even recommends it. I'd wrap an appropriate cache around it (documentation here)。天真地说,那就是:

<% @resources.sort_by(&:created_at).each do |resource| %>
  <% cache resource do %>
    <%= display_mosaic(resource) %>
  <% end %>
<% end %>

但是,也请仔细查看您的 display_mosaic 方法,因为在我看来它正在重新实现内置的多态部分渲染逻辑。周围有一些不稳定的文件名,您可以将其替换为:

<%= render partial: @resources.sort_by(&:created_at), cached: true %>

超高效渲染。

或者,这是一种相当常见的首页模式,使用 materialized view that's a union query and has its own ActiveRecord model. You can manage views in Rails with the Scenic gem.