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.