Rails 7 adds `#with_all_rich_text` to eager load all rich text associations at once


Rails 7 added support for eager loading all rich text associations at once. We can now use #with_all_rich_text instead of eager loading each rich text association separately with #with_rich_text_{field_name}.

Before

We could eager load rich text associations using the helpers provided by ActionText.

Let’s take an example of the Post model. We have 3 rich text fields in the Post model - :summary, :body and :tldr.

class Post < ApplicationRecord
  has_rich_text :summary
  has_rich_text :body
  has_rich_text :tldr
end

Now, if we have to eager load all the rich_text associations we would end up doing the following:

def show
  Post
    .with_rich_text_summary
    .with_rich_text_body
    .with_rich_text_tldr
    .find(params[:id])
end

This will still make 4 queries to the database.

  • 1 query to load the Post and
  • 3 queries to the ActionText table to load the 3 rich_texts associations.

We can see the same in the log below.

Started GET "/posts/1" for ::1 at 2021-03-17 15:25:43 +0530
Processing by PostsController#show as HTML
  Parameters: {"id"=>"1"}
   (0.1ms)  SELECT sqlite_version(*)
   app/controllers/posts_controller.rb:17:in 'show'
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   app/controllers/posts_controller.rb:17:in 'show'
  ActionText::RichText Load (0.2ms)  SELECT "action_text_rich_texts".* FROM "action_text_rich_texts" WHERE "action_text_rich_texts"."record_type" = ? AND "action_text_rich_texts"."name" = ? AND "action_text_rich_texts"."record_id" = ?  [["record_type", "Post"], ["name", "summary"], ["record_id", 1]]
   app/controllers/posts_controller.rb:17:in 'show'
  ActionText::RichText Load (0.1ms)  SELECT "action_text_rich_texts".* FROM "action_text_rich_texts" WHERE "action_text_rich_texts"."record_type" = ? AND "action_text_rich_texts"."name" = ? AND "action_text_rich_texts"."record_id" = ?  [["record_type", "Post"], ["name", "body"], ["record_id", 1]]
   app/controllers/posts_controller.rb:17:in 'show'
  ActionText::RichText Load (0.1ms)  SELECT "action_text_rich_texts".* FROM "action_text_rich_texts" WHERE "action_text_rich_texts"."record_type" = ? AND "action_text_rich_texts"."name" = ? AND "action_text_rich_texts"."record_id" = ?  [["record_type", "Post"], ["name", "tldr"], ["record_id", 1]]

After

The new method #with_all_rich_text allows to load all the rich text associations at once.

With the same Post model with three action_text associations.

class Post < ApplicationRecord
  has_rich_text :summary
  has_rich_text :body
  has_rich_text :tldr
end

We will now use the newly introduced method #with_all_rich_text.

def show
  Post
    .with_all_rich_text
    .find(params[:id])
end

The above method will now only fire one query to the database. This is how the query will be generated.

Started GET "/posts/1" for ::1 at 2021-03-17 15:31:53 +0530
Processing by PostsController#show as HTML
  Parameters: {"id"=>"1"}
  SQL (0.3ms)  SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."body" AS t0_r2, "posts"."created_at" AS t0_r3, "posts"."updated_at" AS t0_r4, "posts"."status" AS t0_r5, "posts"."category" AS t0_r6, "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, "rich_text_bodies_posts"."id" AS t2_r0, "rich_text_bodies_posts"."name" AS t2_r1, "rich_text_bodies_posts"."body" AS t2_r2, "rich_text_bodies_posts"."record_type" AS t2_r3, "rich_text_bodies_posts"."record_id" AS t2_r4, "rich_text_bodies_posts"."created_at" AS t2_r5, "rich_text_bodies_posts"."updated_at" AS t2_r6, "rich_text_tldrs_posts"."id" AS t3_r0, "rich_text_tldrs_posts"."name" AS t3_r1, "rich_text_tldrs_posts"."body" AS t3_r2, "rich_text_tldrs_posts"."record_type" AS t3_r3, "rich_text_tldrs_posts"."record_id" AS t3_r4, "rich_text_tldrs_posts"."created_at" AS t3_r5, "rich_text_tldrs_posts"."updated_at" AS t3_r6 FROM "posts" LEFT OUTER JOIN "action_text_rich_texts" ON "action_text_rich_texts"."record_type" = ? AND "action_text_rich_texts"."name" = ? AND "action_text_rich_texts"."record_id" = "posts"."id" LEFT OUTER JOIN "action_text_rich_texts" "rich_text_bodies_posts" ON "rich_text_bodies_posts"."record_type" = ? AND "rich_text_bodies_posts"."name" = ? AND "rich_text_bodies_posts"."record_id" = "posts"."id" LEFT OUTER JOIN "action_text_rich_texts" "rich_text_tldrs_posts" ON "rich_text_tldrs_posts"."record_type" = ? AND "rich_text_tldrs_posts"."name" = ? AND "rich_text_tldrs_posts"."record_id" = "posts"."id" WHERE "posts"."id" = ? LIMIT ?  [["record_type", "Post"], ["name", "summary"], ["record_type", "Post"], ["name", "body"], ["record_type", "Post"], ["name", "tldr"], ["id", 1], ["LIMIT", 1]]

How this works?

The following method is responsible for finding all the rich_text associations for the model.

private

def rich_text_association_names
  reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?("rich_text_") }
end

It is collecting all the has_one associations for the model starting with rich_text_ and eager loading all the associations at once.