For what I need it
The project has complex business logic, some collections are selected using the find_by_sql
method. Each item in this collection has associations, but selecting data for each item in a loop is not the right way. I already have data and we can add them to each element through build
, so that in the templates we can access through the .<association>
method. However, ActiveRecord continues to torment the database.
Models:
class Offer
has_many :offer_filters
has_many :filters, through: :offer_filters
class Filter
has_many :offer_filters
has_many :offers, through: :offer_filters
rails console
without .build
method load association from DB - good:
offer = Offer.first
Offer Load (0.8ms) SELECT "offers".* FROM …
=> #<Offer:0x0000 …
offer.filters
Filter Load (0.3ms) SELECT "filters".* FROM …
Now let me fill out the association before calling it, and I expect that there will be no query to the database.
offer = Offer.first
Offer Load (0.8ms) SELECT "offers".* FROM …
=> #<Offer:0x0000 …
offer.filters.build [Filter.first.attributes, Filter.second.attributes]
Filter Load (0.8ms) SELECT "filters".* FROM …
Filter Load (0.7ms) SELECT "filters".* FROM …
=> [#<Filter:0x0000…
#<Filter:0x0000…]
offer.filters
Filter Load (0.7ms) SELECT "filters".* FROM "filters" INNER JOIN "offer_filters" # !!!
I would rather not have this last request.
Sory for my english.
Real code:
# Business logic with hard SQL
@offers = Offer.find_by_sql ...
# every offer has normal sortered filter:
offers_with_filters = Offer.includes(:filters).where(id: @offers.map{|o| o.id}).order('filters.order desc')
ioffers_with_filters_id = Hash[offers_with_filters.map{|x| [x.id, x]}]
@offers.map! do |offer|
# Add filters from "normal sort" to offer with hard sql
offer.filters.build ioffers_with_filters_id[offer.id].filters.map{|x| x.attributes}
# THIS LINE RELOAD FILTERS FROM DATABASE and run query in `each` - running database queries in a loop is bad form in programming, no?
offer.filters.each do |filter|
filter.values = filter_values&.[](offer.id)&.[](filter.slug) || []
end
offer
end
I tried to use .build
which is not intended for this.
I found a similar question Preload has_many associations with dynamic conditions
The very statement of the problem is not correct, so the solution is in the form of a monkey patch:
# 1st query: load places
places = Place.all.to_a
# 2nd query: load events for given places, matching the date condition
events = Event.where(place: places.map(&:id)).where("start_date > '#{time_in_the_future}'")
events_by_place_id = events.group_by(&:place_id)
#3: manually set the association
places.each do |place|
events = events_by_place_id[place.id] || []
association = place association(:events)
association.loaded!
association.target.concat(events)
events.each { |event| association.set_inverse_instance(event) }
end