Search code examples
ruby-on-railsactiverecordcollectionsbuildrails-activerecord

Ruby on Rails ActiveRecord. How to prevent taking a collection from the database after the build method?


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

Solution

  • 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