ruby-on-railsmodelassociationseager-loadingnested-includes

Rails nested includes with dynamic class


If I have a class A that has many B, but B is just the ID of a class, and can actually take the form of multiple different classes, can I eager load the associations of B dynamically based off what class it is?

For example, if B was the id of class Car, am I somehow able to eager load B.wheels. But also if it's of type Dog, can I get B.Toys? This would all be through A, which of only one class type. I tried with syntax:

notifs = current_user.notifications.includes(target: [:wager_members, :disputes, :games])

Where notifications would be of class A, :target would be the dynamic class B, and :wager_members, :disputes, and :games would be the associations depending on what class B is.

However, I get an error saying there is no class F for class B, which makes sense because class B dynamically changes. Is there a way/syntax to load all nested associations in one fell swoop?

I'm wondering if I need to rethink model associations to make it feasible.


Solution

  • Polymorphic associations are key here: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations

    Here is roughly what your associations should look like at the class level

    User
    has_many :notifications
    
    Notification
    belongs_to :user
    belongs_to :target
    
    Target
    belongs_to :notify, polymorphic: true # Can be any model
    

    You should then be able to do something like this:

    Notification.create(user: user, target: wager_member)
    Notification.create(user: user, target: dispute)
    Notification.create(user: user, target: game)
    
    notifs = current_user.notifications.includes(target: [:wager_members, :disputes, :games])
    
    notifs.first.target.notify.class.name # "WagerMember"
    

    Really you can skip the target model and move the polymorphic association to the notification itself. Though may be useful if you need to add some custom logic. This results in this:

    Notification
    belongs_to :user
    belongs_to :target, polymorphic: true
    
    notifs.first.target.class.name # "WagerMember"