ActiveRecord Query Interface - Association with conditions - ruby-on-rails

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.

Related

Ordering by a related classes created_at in Rails

I've got a model User with a related class (User has_many Posts, Post belongs_to User). I want to display Users in a list, with those who have posted most recently at the top. So basically I want to order the List of Users by the created_at date of their last post. How is the best way to query this in Rails 3?
Thanks!
I think I would just do this by pulling the Posts, ordering by created_at and doing a group by on the user_id.
Maybe try something like this.
class User < ActiveRecord::Base
has_many :posts
has_one :last_post, :order => 'created_at DESC', :class_name => "Post"
scope :sort_by_last_post_desc, :include => :last_post, :order => ('posts.created_at DESC')
end
NOTE: not tested

ActiveRecord Association without Foreign Key

I am trying to build a relationship model between users. A user can either initiate a relation, or receive a relation from another user. Therefore, the relations table in the db has the foreign keys "initiator_id" and "recipient_id".
Now, I can figure what relations the user initiated or received using the following associations:
has_many :initiated_relations, :foreign_key => :initiator_id, :class_name => 'Relation', :dependent => :destroy
has_many :received_relations, :foreign_key => :recipient_id, :class_name => 'Relation', :dependent => :destroy
What I am trying to do, is build an association that will fetch me all relations that belong to a user (either initiated or received). Trying the following does not work, and complains about the lack of "user_id" field:
has_many :relations, :conditions => 'recipient_id = #{id} or initiator_id = #{id}'
How can I create an association that is solely based on the conditions field, without looking for the default foreign_key? Or is there perhaps a completely different approach to solving this?
From your comments to #neutrino's answer I understand, that you only need this "relation" for read only operations. If you're on Rails 3 you can utilize the fact, that it uses lazy fetching. The where() method returns ActiveRecord::Relation object, which you can later modify. So you can define a method like this:
def User < ActiveRecord::Base
def all_relations
Relation.where("initiator_id => ? OR recipient_id = ?", id, id)
end
end
And then you can do:
User.all_relations.where(:confirmed => true).all
Well, I can think of using finder_sql for that:
has_many :relations, :finder_sql => 'select * from relations right outer join users
on relations.recipient_id = #{id} or relations.initiator_id = #{id}'
Apart from that, you can just write a method that will return a united array of the two relations associations', but you will lose the advantage of an association interface (phew).
Perhaps someone will come up with a better solution.

ActiveRecord siblings in many-to-many relationship

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.

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