ActiveRecord - querying polymorphic associations

Ruby on-RailsRubyActiverecordPolymorphic Associations

Ruby on-Rails Problem Overview


I am using polymorphic associations to track Comments in my project. All very straight forward stuff.

The problem I have is in querying based on the polymorphic association and joining from the Comment model back to it's owner.

So ...

I have a Comment model

class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
end

And a ForumTopics mode:

class ForumTopic < ActiveRecord::Base
  has_many :comments, :as => :commentable
end

I have several other "commentable" models that aren't important right now. All of this works.

What I am trying to do is find all of the Comments that belong to a ForumTopic with a specified condition (in this case, 'featured' == true).

When I try and use a finder to join the models:

@comments = Comment.find(:all 
            :joins => :commentable
            :conditions => ["forum_topics.featured = ? ", true] 
            )

I receive the following error:

> Can not eagerly load the polymorphic association :commentable

Using the AR "include syntax":

@comments = Comment.find(:all 
            :include => :forum_topics
            :conditions => ["forum_topics.featured = ? ", true] 
            )

returns:

> Association named 'forum_topics' was not found; perhaps you misspelled it?

If I try and join with a table name instead of the association name (string instead of symbol):

@comments = Comment.find(:all,
            :joins => "forum_topics",
            :conditions => ["forum_topics.featured = ? ", true] 
            )

I see:

> Mysql::Error: Unknown table 'comments': SELECT comments. FROM comments forum_topics WHERE (forum_topics.featured = 1 )*

(You can see here that the syntax of the underlying query is totally off and the join is missing altogether).

Not sure if what I am doing is even possible, and there are other ways to achieve the required result but it seems like it should be doable.

Any ideas? Anything I am missing?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Argh!

I think I found the problem.

When joining via:

@comments = Comment.find(:all,
        :joins => "forum_topics",
        :conditions => ["forum_topics.featured = ? ", true] 
        )

You need the whole join!

:joins => "INNER JOIN forum_topics ON forum_topics.id = comments.commentable_id",

See the ever-awesome: http://guides.rubyonrails.org/active_record_querying.html#joining-tables

Solution 2 - Ruby on-Rails

An old question, but there is a cleaner way of achieving this by setting up a direct association for the specific type along with the polymorphic:

#comment.rb
class Comment < ActiveRecord::Base

  belongs_to :commentable, polymorphic: true
  belongs_to :forum_topics, -> { where( comments: { commentable_type: 'ForumTopic' } ).includes( :comments ) }, foreign_key: 'commentable_id'

  ...

end

You are then able to pass :forum_topics to includes getting rid of the need for a messy join:

@comments = Comment
  .includes( :forum_topics )
  .where( :forum_topics => { featured: true } )

You could then further clean this up by moving the query into a scope:

#comment.rb
class Comment < ActiveRecord::Base

  ...

  scope :featured_topics, -> { 
    includes( :forum_topics )
    .where( :forum_topics => { featured: true } ) 
  }

  ...

end

Leaving you to be able to simply do

@comments = Comment.featured_topics

Solution 3 - Ruby on-Rails

You Need a Conditional, Plus Rails 3+

A lot of people alluded to it in the answers and comments but I felt that people, including myself, would get tripped up if they landed here and didn't read thoroughly enough.

So, here's the proper answer, including the conditional that is absolutely necessary.

@comments = Comment.joins( "INNER JOIN forum_topics ON comments.commentable_id = forum_topics.id" )
                   .where( comments:     { commentable_type: 'ForumTopic' } )
                   .where( forum_topics: { featured:         true         } )

Thanks to all, especially @Jits, @Peter, and @prograils for their comments.

Solution 4 - Ruby on-Rails

Checked to work under Rails 5:

Solution 1:

@comments = Comment
              .where(commentable_type: "ForumTopic")
              .joins("INNER JOIN forum_topics ON comments.commentable_id = forum_topics.id")
              .where(forum_topics: {featured: true})
              .all

or

Solution 2:

@comments = Comment
              .joins("INNER JOIN forum_topics ON comments.commentable_id = forum_topics.id AND comments.commentable_type = 'ForumTopic'")
              .where(forum_topics: {featured: true}).all

Pay attention to the raw SQL syntax: no backticks are allowed. See http://guides.rubyonrails.org/active_record_querying.html#joining-tables .

I personally prefer Solution 1 as it contains fewer raw SQL syntax.

Solution 5 - Ruby on-Rails

The accepted solution does not work once you introduce another model that has an association using "commentable". commentable_id is not unique and therefore you'll start retrieving the wrong comments.

For example:

You decide to add a news model that accepts comments...

class News < ActiveRecord::Base
   has_many :comments, :as => :commentable
end

Now you may get two records back if you made a comment on a forum_topic with an id of 1 and a news article with an id of 1 using your query:

:joins => "INNER JOIN forum_topics ON forum_topics.id = comments.commentable_id"

You could probably solve the problem by supplying a commentable_type as one of your conditions, but I don't think that's the best way to approach this issue.

Solution 6 - Ruby on-Rails

I came across this post and it lead me to my solution. Using the commentable_type as one of my conditions but using a LEFT OUTER JOIN instead. That way forum topics without comments will be included.

LEFT OUTER JOIN `comments` ON `comments`.`commentable_id` = `forum_topics`.`id` AND `comments`.`commentable_type` = 'ForumTopic'

Solution 7 - Ruby on-Rails

Here is an example to adapt to your purpose.

class AdwordsCampaign < ApplicationRecord
  has_many :daily_campaign_reports, as: :ga_campaign
end

class DailyCampaignReport < ApplicationRecord

  belongs_to :ga_campaign, polymorphic: true

  # scope
  def self.joins_ga_campaign(model)
    models = model.to_s.pluralize
    sql = <<~SQL
      INNER JOIN #{models}
      ON #{models}.id = daily_campaign_reports.ga_campaign_id
    SQL
    joins(sql).where(ga_campaign_type: model.to_s.camelize)
  end
end

Then I can use it wherever needed like this:

DailyCampaignReport.joins_ga_campaign(:adwords_campaign)

Solution 8 - Ruby on-Rails

Rails does not include a polymorphic join by default but this gem would help you to joins your polymorphic relationship with ease. https://github.com/jameshuynh/polymorphic_join

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionToby HedeView Question on Stackoverflow
Solution 1 - Ruby on-RailsToby HedeView Answer on Stackoverflow
Solution 2 - Ruby on-RailsSam PeaceyView Answer on Stackoverflow
Solution 3 - Ruby on-RailsJoshua PinterView Answer on Stackoverflow
Solution 4 - Ruby on-RailsprograilsView Answer on Stackoverflow
Solution 5 - Ruby on-RailsPeterView Answer on Stackoverflow
Solution 6 - Ruby on-RailsChris BarnesView Answer on Stackoverflow
Solution 7 - Ruby on-RailsMax PressView Answer on Stackoverflow
Solution 8 - Ruby on-RailsDamian SiaView Answer on Stackoverflow