Goulven
Goulven

Reputation: 889

How to avoid N+1 when displaying an instance with many action_text fields in Rails?

Rails provides the with_all_rich_text scope to eager load rich_text associated with a collection of active_record objects. It does not provide a method to preload rich text fields at the instance level however, causing N+1 queries.

I've added a custom method to preload other kinds of associations at the instance level using the following:

class ApplicationRecord < ActiveRecord::Base
  def preload(*associations)
    ActiveRecord::Associations::Preloader.new.preload(self, associations.flatten)
    self
  end
end

# Using it in a controller is as easy as:
# 1. this is done in a before_action
@instance = Model.find(id)
# 2. In an action that needs to preload some associations
@instance.preload(:association_a, :association_b, :etc)

I've tried preloading all rich text fields using this helper by passing it the names of the generated rich text associations (:rich_text_a, :rich_text_b) but Rails still fires one query for each rich_text field.

I can leverage the with_all_rich_text scope by combining it with find_by in the controller, but that feels quite clunky, and the generated DB query is really heavy (it contains a LEFT OUTER JOIN for every rich text field).

# In the controller
def show
  @instance = Model.with_all_rich_text.find_by(id: params[:id])
end

Causes the following SQL query:

SELECT 
  "model_table"."id" AS t0_r0,
  "model_table"."attribute_a" AS t0_r1,
  "model_table"."attribute_b" AS t0_r2,
  "model_table"."attribute_c" AS t0_r3,
  "model_table"."attribute_d" AS t0_r4,
  -- repeated for every attribute in my model
  
  "action_text_rich_texts"."id" AS t1_r0,
  "action_text_rich_texts"."name" AS t1_r1,
  "action_text_rich_texts"."body" AS t1_r2,
  "action_text_rich_texts"."record_type" AS t1_r3,
  "action_text_rich_texts"."record_id" AS t1_r4,
  "action_text_rich_texts"."created_at" AS t1_r5,
  "action_text_rich_texts"."updated_at" AS t1_r6
  -- repeated for every rich text field name
FROM "model_table"
  LEFT OUTER JOIN "action_text_rich_texts" ON "action_text_rich_texts"."record_type" = "model_class"
    AND "action_text_rich_texts"."record_id" = "model_table"."id"
    AND "action_text_rich_texts"."name" = "name_of_first_rich_text"
  -- repeated for every rich text field name
WHERE "model_table"."id" = 1 LIMIT 1

Rails log output indicates that ActiveRecord time is roughly the same with and without with_all_rich_text, but using this scope generates about ten times less allocations in my situation, which is clearly better.

Is there a clean way to replicate this scope at the instance level instead of combining it with find_by, like the preload helper I've added?

--

As an aside, when using eager_load, Rails fetches a ton of unused/duplicate data (rich text id, created_at, updated_at, record_type and record_id). The N+1 queries have now been merged into one mega N+1 join, which is better than dozens of queries but still under-optimized.
Would it be possible to have the database return aliased columns for the rich text fields instead?

Upvotes: 1

Views: 563

Answers (1)

Mehmet Celik
Mehmet Celik

Reputation: 51

You may use the includes method from ActiveRecord::QueryMethods

users = User.includes(:address, friends: [:address, :followers])

And if you don't want to query all rich text associations, you only query one field with this method, which you may check from here.

        scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }

Within this method, all included associations will be loaded with your instance.

@instance = Model.includes(:with_rich_text_a, :with_rich_text_b).find_by(id: params[:id])

Upvotes: 1

Related Questions