Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active May 26, 2022 08:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/c54b744a057db50759d38fc0a7979dfb to your computer and use it in GitHub Desktop.
Save JoshCheek/c54b744a057db50759d38fc0a7979dfb to your computer and use it in GitHub Desktop.
How do I define the association in a way that gets rid of this N+1 query?
# Asking the twitterverse how to deal with this: https://twitter.com/josh_cheek/status/1529326801654358023
# Configure ActiveRecord
require 'active_record'
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
# Migrations
ActiveRecord::Schema.define do
self.verbose = false
create_table :users do |t|
t.string :name
t.string :favourite_colour
end
create_table :toys do |t|
t.string :name
t.string :colour
t.references :user
end
end
# Models
class User < ActiveRecord::Base
has_many :toys
has_many :favourite_toys, -> user { where colour: user.favourite_colour }, class_name: 'Toy'
end
class Toy < ActiveRecord::Base
belongs_to :user
end
# Test data
josh = User.create! name: 'Josh', favourite_colour: 'brown'
josh.toys.create! name: 'boat', colour: 'brown'
josh.toys.create! name: 'doll', colour: 'blue'
sally = User.create! name: 'Sally', favourite_colour: 'blue'
sally.toys.create! name: 'block', colour: 'red'
sally.toys.create! name: 'costume', colour: 'brown'
clara = User.create! name: 'Clara', favourite_colour: 'black'
clara.toys.create! name: 'horse', colour: 'black'
clara.toys.create! name: 'mask', colour: 'black'
# Turn on the logger
require 'logger'
ActiveRecord::Base.logger = Logger.new $stdout
ActiveSupport::LogSubscriber.colorize_logging = false
# Test to show that "includes" doesn't fix the N+1 query
User.includes(:favourite_toys).map do |u|
favourite_toys = u.favourite_toys.map(&:name).presence||['none']
"#{u.name}: #{favourite_toys.join(', ')}"
end
# => ["Josh: boat", "Sally: none", "Clara: horse, mask"]
# >> D, [2022-05-24T23:31:50.153921 #89659] DEBUG -- : User Load (0.1ms) SELECT "users".* FROM "users"
# >> D, [2022-05-24T23:31:50.159343 #89659] DEBUG -- : Toy Load (0.1ms) SELECT "toys".* FROM "toys" WHERE "toys"."colour" = ? AND "toys"."user_id" = ? [["colour", "brown"], ["user_id", 1]]
# >> D, [2022-05-24T23:31:50.159724 #89659] DEBUG -- : Toy Load (0.0ms) SELECT "toys".* FROM "toys" WHERE "toys"."colour" = ? AND "toys"."user_id" = ? [["colour", "blue"], ["user_id", 2]]
# >> D, [2022-05-24T23:31:50.159973 #89659] DEBUG -- : Toy Load (0.0ms) SELECT "toys".* FROM "toys" WHERE "toys"."colour" = ? AND "toys"."user_id" = ? [["colour", "black"], ["user_id", 3]]
@benebrice
Copy link

I guest the n+1 query comes from u.favourite_toys because it creates a full new SQL query checking the colour.
Here is my work around.

class User < ActiveRecord::Base
  has_many :toys

  # eagerload to avoid n+1
  def favourite_toys
    toys.select{|t| t.colour == favourite_colour }
  end
end
User.includes(:favourite_toys).map do |u|
  favourite_toys = u.favourite_toys.map(&:name).presence||['none']
  "#{u.name}: #{favourite_toys.join(', ')}"
end

User Load (0.1ms)  SELECT "users".* FROM "users"
Toy Load (0.1ms)  SELECT "toys".* FROM "toys" WHERE "toys"."user_id" IN (?, ?, ?)  [["user_id", 1], ["user_id", 2], ["user_id", 3]]

Since colour can be different for each user, there is not a single query to create but has many queries as users on the users table, so n+1. 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment