Dynamic eager loading associations

Sequel has a concept of dynamic eager loading associations. Not to be wordy, consider the following code example:

class Artist < Sequel::Model
  one_to_many :songs
end

# somewhere in the controller
Artist.eager(songs: -> (ds) { current_user ? ds : ds.where(status: :published) })

The construction with block literally implements this logic: if use is not authenticated there should be visible only published songs, otherwise all songs are visible.

Originally, the business scenario and possible solution in Rails was shared by me in my blog.

Does it make sense to implement something similar in ActiveRecord?

4 Likes

Is CanCanCan not the best fit for this use case?

In your case:

can :read, Song, published: true

See https://github.com/CanCanCommunity/cancancan/blob/develop/docs/fetching_records.md

I don’t think it’s the same. CanCan defines a scope. But the suggestion is for having this thing for associations so that it works with eager loading.

But you can use scopes to eager load relations too. :upside_down_face:

Do you mean calling merge? Something like that?

Artist.includes(:songs).merge(Song.visible_for(current_user))

That might work. But then it comes to the question: what if there is another association, e.g. “artist has many liked_songs” that needs eager loaded but doesn’t need that filter?

Like in the example below:

Artist.includes(:songs, :liked_songs).merge(Song.visible_for(current_user))

I think it definitely makes sense. I would find this useful on a lot of projects where I was working on.

I came here to propose exactly this. I’ve proved out the concept in prior art (occams-record and more recently uberloader), but having proper support in ActiveRecord would be amazing.

I’d be happy to help work on this, but there are a lot of design questions. One difference from the OP is that all my designs so far have used nested blocks for nested associations. An example from uberloader:

widgets = Widget
  .where(category_id: category_ids)
  # Preload category
  .uberload(:category)
  # Preload parts, ordered by name
  .uberload(:parts, scope: Part.order(:name)) do |u|
    # Preload the parts' manufacturer
    u.uberload(:manufacturer)
    # and their subparts, using custom scopes
    u.uberload(:subparts) do
      u.scope my_subparts_scope_helper
      u.scope Subpart.where(type: params[:subparts_types]
) if params[:subparts_types]&.any?

      u.uberload(:foo) do
        u.uberload(:bar)
      end
    end
  end

I know it’s a bit different than how AR traditionally does things, but for giant nested associations I find it a heck of a lot more readable. It also allows for other options besides scope to be added at some point.

But more importantly, this doesn’t (necessarily) have anything to do with authorization, as some have suggested. (Even when it does involve authz, not everyone uses CanCan). It’s a more general need for customizable preloads - there’s a ton of ActiveRecord::Relation power locked away behind those deeply nested Hashes we all pass to preload. Let’s unlock it!

1 Like