left join in rails/mysql - ruby-on-rails

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.

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.

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.

Rails polymorphic many to many association

I'm trying setup a generic sort of web of related objects. Let say I have 4 models.
Book
Movie
Tag
Category
I would like to able to do:
book = Book.find(1)
book.relations << Tag.find(2)
book.relations << Category.find(3)
book.relations #=> [Tag#2, Category#3]
movie = Movie.find(4)
movie.relations << book
movie.relations << Tag.find(5)
movie.relations #=> [Book#1, Tag#5]
Basically I want to be able to take any 2 objects of any model class (or model class that I allow) and declare that they are related.
Obviously I don't want to create a huge mess of join tables. This seems like it's not quite a has many through association, and not quite a polymorphic association.
Is this something that Rails can support via it's association declarations or should I be rolling my own logic here?
Support for polymorphism has improved dramatically since the early days. You should be able to achieve this in Rails 2.3 by using a single join table for all your models -- a Relation model.
class Relation
belongs_to :owner, :polymorphic => true
belongs_to :child_item, :polymorphic => true
end
class Book
has_many :pwned_relations, :as => :owner, :class_name => 'Relation'
has_many :pwning_relations, :as => :child_item, :class_name => 'Relation'
# and so on for each type of relation
has_many :pwned_movies, :through => :pwned_relations,
:source => :child_item, :source_type => 'Movie'
has_many :pwning_movies, :through => :pwning_relations,
:source => :owner, :source_type => 'Movie'
end
A drawback of this kind of data structure is that you are forced to create two different roles for what may be an equal pairing. If I want to see all the related movies for my Book, I have to add the sets together:
( pwned_movies + pwning_movies ).uniq
A common example of this problem is the "friend" relationship in social networking apps.
One solution used by Insoshi, among others, is to register an after_create callback on the join model ( Relation, in this case ), which creates the inverse relationship. An after_destroy callback would be similarly necessary, but in this way at the cost of some additional DB storage you can be confident that you will get all your related movies in a single DB query.
class Relation
after_create do
unless Relation.first :conditions =>
[ 'owner_id = ? and owner_type = ? and child_item_id = ? and child_item_type = ?', child_item_id, child_item_type, owner_id, owner_type ]
Relation.create :owner => child_item, :child_item => owner
end
end
end
I have come up with a bit of solution. I'm not sure it's the best however. It seems you cannot have a polymorphic has_many through.
So, I fake it a bit. But it means giving up the association proxy magic that I love so much, and that makes me sad. In a basic state, here is how it works.
book = Book.find(1)
book.add_related(Tag.find(2))
book.add_related(Category.find(3))
book.related #=> [Tag#2, Category#3]
book.related(:tags) #=> [Tag#2]
I wrapped it up in a reusable module, that can be added to any model class with a single has_relations class method.
http://gist.github.com/123966
I really hope I don;t have to completely re-implement the association proxy to work with this though.
I think the only way to do it exactly as you described is the join tables. It's not so bad though, just 6, and you can pretty much set-and-forget them.
depending on how closesly related your movies/books db tables are
what if you declared
class Items < ActiveRecord::Base
has_many :tags
has_many :categories
has_and_belongs_to_many :related_items,
:class => "Items",
:join_table => :related_items,
:foreign_key => "item_id",
:associated_foreign_key => "related_item_id"
end
class Books < Items
class Movies < Items
make sure you put type in your items table

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