Created
June 8, 2022 23:31
-
-
Save JoshCheek/9fb0e9a54adb718e626e8e3dcd33e439 to your computer and use it in GitHub Desktop.
ActiveRecord: Comparing different ways to load associated data (`includes` vs `preload` vs `eager_load`)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Config | |
require 'active_record' | |
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:' | |
# Migrations | |
ActiveRecord::Schema.define do | |
self.verbose = false | |
create_table(:users) { |t| t.string :name } | |
create_table :posts do |t| | |
t.string :name | |
t.integer :user_id | |
end | |
end | |
# Models | |
class User < ActiveRecord::Base | |
has_many :posts | |
def self.to_text | |
all.map { |u| "#{u.name}: #{u.posts.pluck(:name).join(', ')}" }.join(', ') | |
end | |
end | |
class Post < ActiveRecord::Base | |
belongs_to :user | |
end | |
# Helper method so we can see what queries it ran | |
def self.capture_queries(&block) | |
queries = [] | |
capturer = -> *, data { queries << data.values_at(:name, :sql).select(&:present?).join(": ") } | |
ActiveSupport::Notifications.subscribed capturer, "sql.active_record", &block | |
queries | |
end | |
# Seed data | |
User.create! name: 'Josh', posts: [Post.new(name: "josh's post")] | |
User.create! name: 'Becky', posts: [Post.new(name: "becky post")] | |
# ===== Comparing Different Ways To Access Associated Data ===== | |
# Doing nothing leads to N+1 | |
capture_queries do | |
User.all.to_text # => "Josh: josh's post, Becky: becky post" | |
end | |
# => ["User Load: SELECT \"users\".* FROM \"users\"", | |
# "Post Pluck: SELECT \"posts\".\"name\" FROM \"posts\" WHERE \"posts\".\"user_id\" = ?", | |
# "Post Pluck: SELECT \"posts\".\"name\" FROM \"posts\" WHERE \"posts\".\"user_id\" = ?"] | |
# Use `eager_load` to do a left outer join. | |
# This may be what you were looking for, it's similar to your example (note that | |
# this is not necessarily better, it saves a query, but could potentially lead to | |
# many rows of almost entirely duplicated data being returned for each record) | |
capture_queries do | |
User.eager_load(:posts).to_text # => "Josh: josh's post, Becky: becky post" | |
end | |
# => ["SQL: SELECT \"users\".\"id\" AS t0_r0, \"users\".\"name\" AS t0_r1, \"posts\".\"id\" AS t1_r0, \"posts\".\"name\" AS t1_r1, \"posts\".\"user_id\" AS t1_r2 FROM \"users\" LEFT OUTER JOIN \"posts\" ON \"posts\".\"user_id\" = \"users\".\"id\""] | |
# I'll reformat it so it's easier to see how it works: | |
.first[5..].gsub(/(?=FROM|LEFT)/,"\n").gsub(/(?<=,)/, "\n ") | |
# => "SELECT \"users\".\"id\" AS t0_r0,\n" + | |
# " \"users\".\"name\" AS t0_r1,\n" + | |
# " \"posts\".\"id\" AS t1_r0,\n" + | |
# " \"posts\".\"name\" AS t1_r1,\n" + | |
# " \"posts\".\"user_id\" AS t1_r2 \n" + | |
# "FROM \"users\" \n" + | |
# "LEFT OUTER JOIN \"posts\" ON \"posts\".\"user_id\" = \"users\".\"id\"" | |
# Use `preload` to batch the association queries into a single additional query | |
capture_queries do | |
User.preload(:posts).to_text # => "Josh: josh's post, Becky: becky post" | |
end | |
# => ["User Load: SELECT \"users\".* FROM \"users\"", | |
# "Post Load: SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" IN (?, ?)"] | |
# Use `includes` most of the time, it will choose between `eager_load` and `preload`, based on simple heuristics. | |
# It chooses `preload` by default | |
capture_queries do | |
User.includes(:posts).to_text # => "Josh: josh's post, Becky: becky post" | |
end | |
# => ["User Load: SELECT \"users\".* FROM \"users\"", | |
# "Post Load: SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" IN (?, ?)"] | |
# Here, we've implied we need the association in the query (via `references`), so it chooses `eager_load` | |
capture_queries do | |
User.includes(:posts).references(:posts).to_text # => "Josh: josh's post, Becky: becky post" | |
end | |
# => ["SQL: SELECT \"users\".\"id\" AS t0_r0, \"users\".\"name\" AS t0_r1, \"posts\".\"id\" AS t1_r0, \"posts\".\"name\" AS t1_r1, \"posts\".\"user_id\" AS t1_r2 FROM \"users\" LEFT OUTER JOIN \"posts\" ON \"posts\".\"user_id\" = \"users\".\"id\""] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment