Skip to content

Instantly share code, notes, and snippets.

@pmn4
Last active April 4, 2020 00:51
Show Gist options
  • Save pmn4/eb497edad63065304383bdfcf8d60b47 to your computer and use it in GitHub Desktop.
Save pmn4/eb497edad63065304383bdfcf8d60b47 to your computer and use it in GitHub Desktop.
Inflate an array of models with a foreign key relationship in a single query (relationship: Belongs To)
module InflateBelongsTo
# when you have an array of models, but want to include (or even map)
# an associated resource, this method puts together a single query to
# fetch the data, then appends it to the model, cutting down on total
# database calls.
# this does the exact same work as eager loading when you first query
# for a set of models, but is especially useful if you the original
# query is uneditable (not written by you) or you wish to get results
# conditionally (i.e., you only want _some_ associations loaded)
#
# models: the array of models whose foreign key relationship you wish
# to inflate
# key: the instance method used to get the foreign record
# key is used to construct two methods:
# - `#{key}_id` for getting foreign record's id
# - `#{key}=` for assignment once we've fetched
#
# returns an array of all resources
def inflate(models, key = self.name.underscore.to_sym, resources: nil)
key_id = :"#{key}_id"
models = Array(models)
resources ||= begin
ids = models
.select { |m| m.respond_to?(key_id) }
.map { |m| m.send(key_id) }
.uniq
.compact
where(id: ids)
end
if resources.present?
resource_map = resources.map { |r| [r.id, r] }.to_h
models
.select { |m| m.respond_to?(:"#{key}=") }
.each { |m| m.send(:"#{key}=", resource_map[m.send(key_id)]) }
.each { |m| m.association(key).loaded! }
end
resources
end
end
@pmn4
Copy link
Author

pmn4 commented Jun 7, 2019

To use this code, consider a scenario where User instances have a related Profile:

# you may only need
User.includes(:profile).all # results in 2 queries, 1 for Users, 1 for related Profiles

Sometimes, however, you don't have the ability to edit the original query.

# users is an array of Users, generated somewhere else in code

# this is the scenario we are trying to avoid:
users.each { |u| puts u.profile } # results in n queries, one for each User in users

# by first "inflating", we eliminate n-1 queries
Profile.inflate(users, :profile) # results in 1 query for all related Profiles
users.each { |u| puts u.profile } # results in 0 queries

This uses a single query to get all profiles.

Advanced Usage #1:

Consider that a User can have a parent User:

# users is an array of Users, generated somewhere else in code
all_users = users.flat_map { |u| [self, u.parent] }
Profile.inflate(all_users, :profile) # results in 1 query for all related Profiles
all_users.each { |u| puts u.profile, u.parent.profile } # will result in 0 queries

Advanced Usage #2:

Consider that you only want the profiles created within the last year:

# users is an array of Users, generated somewhere else in code
Profile.where('created_at > ?', 1.year.ago).inflate(users, :profile)

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