ActiveRecord siblings in many-to-many relationship - ruby-on-rails

I have this working to a degree, but I am looking for some input on how to query for the siblings in a one to many relationship to see if there is a more elegant way of accomplishing this.
Consider the following classes
class Post < ActiveRecord::Base
has_many :post_categories
has_many :categories, :through => :post_categories
end
class Category < ActiveRecord::Base
has_many :post_categories
has_many :posts, :through => :post_categories
end
A post by definition can have multiple categories, what I would need this for is to show a "related posts" area on the site. Like I mentioned previously I do have a working version which is to simply do the following:
Post.find(id, :include => {:categories => :posts})
Looking at the logs the application then has to do five queries to get the end data that I am looking for.
Any thoughts are appreciated!

The only problem I see you having with what you have already is that you probably don't want to return all posts which share a category with a post.
#post = Post.find params[:id]
#related_posts = Posts.find(:all, :joins => :post_categories,
:select => "posts.*, count(post_categories) post_category_count",
:conditions => {:post_categories => {:category => #post.categories}},
:group => "posts.id", :order => "post_category_count desc")
This will return the most relevant posts first, ie. those which have the most shared categories, and you can either add a limit or paginate in order to limit results returned.

If you need support for large object trees, you might want to look at awesome nested set thought it may be overkill for this problem.

Related

ActiveRecord Query Interface - Association with conditions

I have a Post model that has_many :comments. The question is: how can I build a query using ActiveRecord Query Interface that retrieves all posts and the last comment of each posts?
All I have right now is the following, but it doesn't filter the comments and I have no idea of what to do next:
Post.includes(:comments)
Well, the simplest thing comes to mind, which may not be the best is creating a scope.
class Post < ActiveRecord::Base
# has_many or maybe a has_one ? I don't have time to fire up console again to check, sorry!
has_many :last_comment, :class_name => 'Comment', :limit => 1, :order => 'comments.created_at DESC'
end
And then use
Post.includes(:last_comment)
Be sure you have indexes in your SQL.

Rails: has_many Active record query

I'm wondering how you would simulate a :has_many :through using an AR call.
Ultimately, I'm trying to find the subcategories that belong to a site, not the top level categories, which is what the query currently gives me.
Models:
class Categories < AR
:has_many :subcategories, :through => :cat_subcat_links
:has_many :cat_subcat_links
end
Linking Model:
class CatSubcatLinks < AR
:category_id
:subcategory_id
:site_id
end
At the moment, if I want to get which categories belong to a particular site I perform:
Category.joins(:cat_subcat_links).where(:cat_subcat_links => {:site_id => 1})
The query that returns:
SELECT `categories`.* FROM `categories` INNER JOIN `cat_sub_links` ON `cat_sub_links`.`category_id` = `categories`.`id` WHERE `cat_sub_links`.`site_id` = 1
The problem is
`cat_sub_links`.`category_id` = `categories`.`id`
I need it to say
`cat_sub_links`.`subcategory_id` = `categories`.`id`
That will cause the query to return me the subcategories. Thoughts?
Thanks in advanced,
Justin
Assuming you have in your site.rb
class Site < AR
has_many :cat_subcat_links
has_many :subcategories, :through => :cat_subcat_links
has_many :categories, :through => :cat_subcat_links
end
Couldn't you just do:
Site.find([your_site_id]).categories
Also:
Site.find([your_site_id]).subcategories
Also just a thought; are you sure it wouldn't be better to use acts_as_tree or awesome_nested_set instead?
Ultimately, I had to write the join instead since I can't specify a :source like I normally would if I created the relationship.
Category.joins("JOIN cat_sub_links ON cat_sub_links.subcategory_id=categories.id").where(: cat_sub_links => {:site_id => #site_ids})
I'll leave the answer out for a few days if anyone has a more elegant solution.

Rails polymorphic associations, two assoc types in one class

Consider a class:
class Link < ActiveRecord::Base
has_many :link_votes, :as => :vote_subject, :class_name => 'Vote'
has_many :spam_votes, :as => :vote_subject, :class_name => 'Vote'
end
The problem is, when I'm adding a new vote with #link.link_votes << Vote.new the vote_subject_type is 'Link', while I wish it could be 'link_votes' or something like that. Is this an AR limitation or is there a way to workaround this thing?
I've actually found one related answer, but I'm not quite sure about what it says: Polymorphic Association with multiple associations on the same model
Sounds like you want to use Single Table Inheritance - this will allow you to have two different types of Votes. This will add a 'type' column to the votes table that you will then access as a LinkVote or SpamVote
class SpamVote << Vote
...
end
Along those lines.
class Link < ActiveRecord::Base
has_many :link_votes, :as => :vote_subject
has_many :spam_votes, :as => :vote_subject
end
In the votes table you'd see columns like:
id, type, vote_subject_type, vote_subject_id, etc.
Do more research on STI and I bet you'll find your answer.

left join in rails/mysql

i am newbie in rails and try to perform left join in mysql.
there are two objects - user and message.
user has_and_belongs_to_many messages, message has_and_belongs_to_many users
currently, by simply writing user.messages i get following query in console
SELECT * FROM `messages` INNER JOIN `messages_users` ON `messages`.id = `messages_users`.message_id WHERE (`users_messages`.group_id = 1 )
Message with restricted==false is not connected to any user but is accessible by any user, and i need to add collection Message.all(restricted=>false) to user.messages
the query which would solve my problem would be:
select * from messages left join messages_users on messages_users.message_id=messages.id and messages_users.user_id=1 where (messages_users.user_id is NULL and messages.restricted=false) OR (messages_users.user_id=1 and messages.restricted=true);
how do i write it in rails as elegantly as possible?
would it be smth like
Message.find(:all,:conditions => "(messages_users.user_id is NULL and messages.restricted=false) OR (messages_users.user_id=1 and messages.restricted=true)", :joins => "left join messages_groups on messages_users.message_id=messages.id and messages_users.user_id=1 " )
or can it be nicer?
i am using rails 2.3.2
thanks,
Pavel
It seems to me that you are trying to pull two things in that query: 1) all messages not tied to a user with restricted=false and 2) all messages tied to the current user with restricted=true.
If I'm understanding that correctly, I don't see a better way to do it if you want it done as a single query. However, if you are open to the idea of making it two queries, you can clean it up slightly in code (while possibly slowing down the execution). Here is the alternative setup:
class Message < ActiveRecord:Base
has_and_belongs_to_many :users
named_scope :restricted, :conditions => {:restricted => true}
named_scope :unrestricted, :conditions => {:restricted => false}
named_scope :public, :conditions => "id NOT IN (SELECT DISTINCT message_id FROM messages_users)"
end
class User < ActiveRecord:Base
has_and_belongs_to_many :messages
end
To get the list in less code, but requiring two database hits you can do:
#current_user.messages.restricted + Message.unrestricted.public
In either case if there is any substantial amount of data in these tables you need to make sure they are properly indexed or this is going to slow down with any load. If this is an app with a lot of messages being sent you're probably better off with the single query. If it's just a side function that will not be used very much, I would probably take the cleaner version.
What would probably work better from a model perspective is to get rid of the HABTM relationship and model the relationship explicitly. Then you have a handy place to keep track of other data about the message sending/delivery/receiving process (like tracking time sent, time read, etc). It doesn't change any of the discussion above, I just prefer has many through to HABTM.
class Message < ActiveRecord:Base
has_many :subscriptions
has_many :users, :through => :subscriptions
named_scope :restricted, :conditions => {:restricted => true}
named_scope :unrestricted, :conditions => {:restricted => false}
named_scope :public, :conditions => "id NOT IN (SELECT DISTINCT message_id FROM subscriptions)"
end
class User < ActiveRecord:Base
has_many :subscriptions
has_many :messages, :through => :subscriptions
end
class Subscription < ActiveRecord:Base
belongs_to :message
belongs_to :user
end
why not use :include ?
I think named_scopes might be your answer. In your model put something like:
named_scope :restricted, :conditions => {:restricted => true}
named_scope :unrestricted, :conditions => {:restricted => false}
Then you can call things like:
Message.restricted
=> All restricted messages
User.first.messages.unrestricted
=> All unrestricted messages belonging to the first user.
I believe these work through HABTM associations.

Loading ONE record for has_many and checking it

I'm implementing a Blog with Post and votable Comments.
When loading a Post, I want to eagerly load all votes by the current user for the Post's Comments.
Something like this (which doesn't work):
#post.comments.all(:joins => :votes, :conditions => ['votes.user_id = ?', current_user.id])
Each Comment has a method called rated_by?
def rated_by?(actor)
votes.find_by_user_id(actor.id)
end
The problem is that ActiveRecord will run a query for each rated_by? call, even though my #post.comments finder joined all the relevant votes.
I had a look at the act_as_rateable plugin but it has the same problem, running a query for each record, not using joins.
Double Secret Edit: I was answering another question and came across something that should work for you. It's a bit of a crazy hack involving the Thread.current global hash. And probably not advised at all, but it works.
It involves creating a second has_many votes association on Comments
class Comment < ActiveRecord::Base
has_many :votes
belongs_to :post
has_many :current_user_votes, :class_name => "Vote",
:conditions => '`#{Vote.table_name}`.user_id = \
#{Thread.current[:current_user].id}'
end
It also requires you to set Thread.current[:current_user] = current_user in the controller where you're going to be calling these methods.
Then you should be able to do
#post.comments.find(:all, :include => :current_user_votes)
To get a list of comments, that have eager loaded only the :current_user_votes. All in one query. If you're getting multiple posts at once, you can do this.
Post.find(:all, :include => { :comments => :current_user_votes},
:conditions => ...)
Which will populate a list of posts, and eager load their comments which in turn will each have their current_user_votes eager loaded.
Original Answer (preserved for posterity)
I don't think it's possible to select all of one model eager load only the relevant associations in one query.
The best you're going to get is pretty much what you've done. Select all of one model and then for each them load only the relevant association with a named scope or finder.
This statement that doesn't work is only selecting comments the user has voted on.
#post.comments.all(:joins => :votes,
:conditions => ['votes.user_id = ?', current_user.id])
This statement selects the same set of comments, but also eager loads all votes for the comments it selects.
#post.comments.all(:include => :votes,
:conditions => ['votes.user_id = ?', current_user.id])
Really what you're going to have to do is call rated_by? on each comment. You might be able to minimize database impact by using a named scope. But I honestly don't think it's going to make an improvement.
If you're so worried about hitting the database so hard you could do something like this:
class Post < ActiveRecord::Base
has_many :comments
has_many :votes, :through => :comments
...
end
class Vote < ActiveRecord::Base
belongs_to :comments
...
named_scope :made_by_user, lambda {|user|
{:conditions => {:user_id => user}}
}
end
#users_votes = #post.votes.made_by_use(current_user)
#comments = #post.comments.find(:all, :include => :votes)
#comments.each{|comment|
user_voted_this_on_this_comment = comment.votes & #user_votes
...
}
Honestly I don't think it's worth the effort.
P.S. There's a Ruby convention regarding methods names that end in a question mark should always return a boolean value.
you need to use
:include => :votes
joins doesn't load your data, it just join the query in the db.

Resources