Rails: Include all of model A and only a subset of related model B

让人想犯罪 __ 提交于 2019-12-12 19:51:25

问题


I'm trying to understand the rails database API better. If I have the following models:

class Link
  has_many :votes
  belongs_to :user
end

class Vote
  belongs_to: link
  belongs_to :user
end

class User
  has_many :links
  has_many :votes
end

I want to "eagerly" return a list of ALL links and for each link I want the votes where the user_id equals the current user. I tried this:

l = Link.all(:include => :votes, :conditions => { :votes => { :user_id => current_user.id}})

But that only returns a list of links for which the user has submitted a vote. I want it to return all the links and then only the votes for that user (or none if there are none). How can I do this with the include statement?


回答1:


I had to reread the question a couple of times so please let me know if I'm confused about this. It sounds like you want to use a LEFT OUTER JOIN. That will give you everything from the links table where the vote matches the user id, and every other link where there is no vote match with the curret user. Something like this:

Link.all(:include => :votes, 
:select=>"DISTINCT links.*",
:joins => "LEFT OUTER JOIN votes ON links.id = votes.link_id AND votes.user_id = #{current_user.id}", 
:conditions=>"NOT EXISTS (SELECT * FROM votes WHERE votes.link_id = links.id) OR votes.user_id = #{current_user.id}")

Also, since you're already using Rails 3.2, you'll probably want to use the newer active record query methods, since I believe the old hash based finder methods are deprecate in Rails 4. So the same query would look like this:

Link.include(:votes)
.joins("LEFT OUTER JOIN votes ON links.id = votes.link_id AND votes.user_id = #{current_user.id}")
.where("NOT EXISTS (SELECT * FROM votes WHERE votes.link_id = links.id) OR votes.user_id = #{current_user.id}")
.uniq.load

Regarding the conditions / where portion, that will create a dependent subquery that will cause the Link row to be rejected if it exists in the votes table (meaning SOMEBODY voted on it at one point), or if it does exist whether it is a vote by our specified user. The end result is you get any Link row that does not have any votes, or links with votes created by the specified user.

UPDATE

As mentioned in the comments, the problem with this approach is that the associated votes rows are not eager loaded as required. After tooling around with this for about 2 hours now, I am nearly to the point of admitting that I do not believe it is possible to eager load the associated table with this kind of query.

If we ditch the include part (which seems to have no effect anyways), we do get back a collection of Link objects that have been voted on by the specified user, or nobody at all. The problem is when we invoke .votes, Rails happily makes another call to the database along the lines of SELECT votes.* FROM votes WHERE votes.link_id = [the link id], which as you can see doesn't discriminate by user_id at that point.

The only solution I can think of is to properly quantify what you're looking for when invoking the association method. Something like this:

link.votes.where(:user_id=>current_user.id)

Which will yield a query like this:

SELECT votes.* FROM votes WHERE votes.link_id = [the link ID] AND user_id = [the user ID]

If anybody has a better solution, I'd be interested in seeing it.

Hope that helps.



来源:https://stackoverflow.com/questions/19393111/rails-include-all-of-model-a-and-only-a-subset-of-related-model-b

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!