Preload associated records

What is the feature?

Defining a list of association to include by default when loading the collection, or even a base scope that would be the beginning of any chain of filtering, searching, etc.

What problem does this solve for you?

Currently it doesn’t seem possible to automatically join associated models easily with a collection. For instance we hit a case where exporting a collection that contains smart fields that are read for an associated model would load each of those associated models individually, each time the smart field was read. This is a classic N+1 situation, where we could solve that in ActiveAdmin by defining the list of associations to include. Those would only be loaded if the displayed view actually hits one of the associations at some point, and would not be loaded if the association isn’t actually used in the current view.

The implementation of a similar method for Forest collections would be amazing. Another option would be to define a scope to use for a specific collection in Forest, and all segments would be chained on it.

To explain a bit more, here is some examples :

Current situation

class Company < ApplicationRecord
  has_one :address
  
  scope :french, -> { joins(:address).where(address: { country: "FRA" }) }
  scope :visible, -> { where(visible: true }
end

class Forest::Company < BaseCollection
  collection :Company
  
  segment "French", scope: :french
  segment "Publicly visible", scope: :visible
  
  field :country, type: "String" do 
    object.address.country
  end
end

In this case :

  • exporting from the collection view will load addresses one by one
  • exporting from the french segment will have already joined the address and will not load one by one
  • exporting from visible will also load addresses one by one

Solution 1: Defining includes

class Company < ApplicationRecord
  has_one :address
  
  scope :french, -> { joins(:address).where(address: { country: "FRA" }) }
  scope :visible, -> { where(visible: true }
end

class Forest::Company < BaseCollection
  collection :Company
  
  includes :address

  segment "French", scope: :french
  segment "Publicly visible", scope: :visible
  
  field :country, type: "String" do 
    object.address.country
  end
end

In this case, all exports would preload all addresses in a single request, before generating the export. At the same time, if the view for Publicly visible doesn’t use the country smart field, then the addresses are not preloaded.

Solution 2: Defining a base scope

class Company < ApplicationRecord
  has_one :address
  
  scope :forest, -> { includes(:address) }
  scope :french, -> { joins(:address).where(address: { country: "FRA" }) }
  scope :visible, -> { where(visible: true }
end

class Forest::Company < BaseCollection
  collection :Company
  
  # possible syntax n°1: referring to an existing scope in the model. very nice, because we can write tests on that scope
  base_scope :forest

  # possible syntax n°2: defining the scope on the fly, could be a bit more flexible, but we loose the ability to write tests on that scope
  base_scope -> { includes(:address) }

  segment "French", scope: :french
  segment "Publicly visible", scope: :visible
  
  field :country, type: "String" do 
    object.address.country
  end
end

In this solution, any segment would apply its scope starting on the base_scope; as such :

  • the collection view would load Company.forest
  • the “French” view would load Company.forest.french
  • the “Publicly visible” view would load Company.forest.visible

Other solutions we discarded

Defining a default scope on the ActiveRecord model

This could be an option, the problem is it ends up polluting our application code that already knows how to properly avoid N+1 queries in the views and APIs where collections of models are loaded.

Who else would be using this feature?

Any one that wants to solve performance issues with smart fields that use associated models. In the case of defining a base scope instead of a list of includes, it could also allow exporting only a partial collection to forest, defined in code and testable.