Complex Rails queries across multiple tables, unions, and will_paginate. Solved.

Posted by uberllama on Stack Overflow See other posts from Stack Overflow or by uberllama
Published on 2010-05-28T15:59:51Z Indexed on 2010/05/28 16:01 UTC
Read the original article Hit count: 370

Hi folks. I've been working on a complex "user feed" type of functionality for a while now, and after experimenting with various union plugins, hacking named scopes, and brute force, have arrived at a solution I'm happy with. S.O. has been hugely helpful for me, so I thought I'd post it here in hopes that it might help others and also to get feedback -- it's very possible that I worked on this so long that I walked down an unnecessarily complicated road.

For the sake of my example, I'll use users, groups, and articles. A user can follow other users to get a feed of their articles. They can also join groups and get a feed of articles that have been added to those groups. What I needed was a combined, pageable feed of distinct articles from a user's contacts and groups.

Let's begin.

user.rb

has_many :articles
has_many :contacts
has_many :contacted_users, :through => :contacts
has_many :memberships
has_many :groups, :through => :memberships

contact.rb

belongs_to :user
belongs_to :contacted_user, :class_name => "User", :foreign_key => "contacted_user_id"

article.rb

belongs_to :user
has_many :submissions
has_many :groups, :through => :submissions

group.rb

has_many :memberships
has_many :users, :through => :memberships
has_many :submissions
has_many :articles, :through => :submissions

Those are the basic models that define my relationships. Now, I add two named scopes to the Article model so that I can get separate feeds of both contact articles and group articles should I desire.

article.rb

# Get all articles by user's contacts
named_scope :by_contacts, lambda {|user|
  {:joins => "inner join contacts on articles.user_id = contacts.contacted_user_id",
  :conditions => ["articles.published = 1 and contacts.user_id = ?", user.id]}
}  

# Get all articles in user's groups. This does an additional query to get the user's group IDs, then uses those in an IN clause
named_scope :by_groups, lambda {|user|
  {:select => "DISTINCT articles.*",
  :joins => :submissions,
  :conditions => {:submissions => {:group_id => user.group_ids}}}
}

Now I have to create a method that will provide a UNION of these two feeds into one. Since I'm using Rails 2.3.5, I have to use the construct_finder_sql method to render a scope into its base sql. In Rails 3.0, I could use the to_sql method.

user.rb

def feed
  "(#{Article.by_groups(self).send(:construct_finder_sql,{})}) UNION (#{Article.by_contacts(self).send(:construct_finder_sql,{})})"
end

And finally, I can now call this method and paginate it from my controller using will_paginate's paginate_by_sql method.

HomeController.rb

@articles = Article.paginate_by_sql(current_user.feed, :page => 1)

And we're done! It may seem simple now, but it was a lot of work getting there. Feedback is always appreciated. In particular, it would be great to get away from some of the raw sql hacking. Cheers.

© Stack Overflow or respective owner

Related posts about mysql

Related posts about ruby-on-rails