0

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
Zlatov
  • 126
  • 1
  • 6
  • `offer.filters` => `offer.filters; 1` and you don't have it :) – Pavel Mikhailyuk May 13 '20 at 10:24
  • nice, but i get '1' instead Filter::ActiveRecord_Association_Collection... class )) Now i found `builded = association_instance_get(:filters).target`, https://stackoverflow.com/questions/47407542/how-to-make-low-level-caching-collaborate-with-association-caching-in-rails?answertab=oldest#tab-top , but it doesn’t work out as I would like – Zlatov May 13 '20 at 11:39
  • I mean your question isn't clear at all as the last DB query is caused by Rails console side effect, not by `offer.filters` code itself. – Pavel Mikhailyuk May 13 '20 at 12:15
  • How about if I said that the code is executed not in the console, but in the controller? `offer.filters.each{|.....` will not be able to take data anywhere except in the database, and run query... – Zlatov May 13 '20 at 13:09
  • I believe that example of your real code (from the controller) will make your question more clear. – Pavel Mikhailyuk May 13 '20 at 13:45
  • 1
    Try `ActiveRecord::Associations::Preloader.new.preload(@offers, :filters)`. – Pavel Mikhailyuk May 14 '20 at 08:54

1 Answers1

0

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
Zlatov
  • 126
  • 1
  • 6