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.