Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditionally eager loading polymorphic associations

I'm trying to eliminate some N+1 queries for a user feed on my site. There are various models that have a polymorphic association to a FeedItem model

class Event < ActiveRecord::Base
  has_many :feed_items, as: :feedable
end

class Tournament < ActiveRecord::Base
  has_many :feed_items, as: :feedable
end

class FeedItem < ActiveRecord::Base
  belongs_to :feedable, :polymorphic => true
end

A user model is also involved where a user has_many Tournaments and Events which both have different attributes. When loading FeedItems for a user how do I conditionally eager load the nested attributes based on which FeedItem type it is?

An example of what I mean (with squeel DSL):

FeedItem.where{ (feedable_id.in(@user.tournaments.ids}) & feedable_type.eq('Tournament')) |    
                (feedable_id.in(@user.events.ids) & feedable_type.eq('Event')) }.includes(:feedable => :game)

This query attempts to retrieve all FeedItems of type Tournament and Event while eager loading the 'game' attribute of the Tournament model. Works fine for Tournaments but when iterating over the FeedItems the events will throw an error that this attribute doesn't exist.

My current hack is to do separate queries for every FeedItem type and add the records together. However this converts the association into an array which I don't want to do.

Any ideas?

like image 268
Tom Avatar asked Oct 21 '13 00:10

Tom


Video Answer


1 Answers

It's possible to preload nested associations for polymorphic associations. You need to add two more associations to FeedItem:

class FeedItem < ActiveRecord::Base
  belongs_to :feedable, :polymorphic => true

  belongs_to :event, -> { where(feedable_type: 'Event' ) }, foreign_key: :feedable_id
  belongs_to :tournament, -> { where(feedable_type: 'Tournament' ) }, foreign_key: :feedable_id
end

After that you can preload nested association only for tournament:

feeds =
  FeedItem.where("
    (feedable_id IN (?) AND feedable_type = 'Tournament') OR 
    (feedable_id IN (?) AND feedable_type = 'Event')", @user.tournament_ids, @user.event_ids).
    includes(:feedable, tournament: :game)

Now you may get related tournament game without additional query:

feeds.each do |feed|
  puts feed.feedable.inspect
  puts feed.tournament.game.inspect if feed.feedable_type == "Tournament"
end
like image 118
Ilya Lavrov Avatar answered Nov 03 '22 06:11

Ilya Lavrov