I'm still having trouble building complex joins in ActiveRecord.
I have a User model that is using the HasManyFriends plugin by Steve Ehrenberg (http://dnite.org).
Then I have a UserFeedEvent model that links users to a FeedEvent model.
What I'd like to achieve is to find all the FeedEvents linked to the friends of a User.
How should I write my ActiveRecord query?
Here are my models:
class User < ActiveRecord::Base
has_many_friends
has_many :feed_events, :through => :user_feed_events, :dependent => :destroy
has_many :user_feed_events, :dependent => :destroy
end
class UserFeedEvent < ActiveRecord::Base
belongs_to :feed_event, :dependent => :destroy
belongs_to :user
end
class FeedEvent < ActiveRecord::Base
has_many :user_feed_events, :dependent => :destroy
has_many :users, :through => :user_feed_events
serialize :data
end
Thanks in advance!
Augusto
Digging through HasManyFriends source leads me to believe that the following should work (or be half-way through):
EDIT: found out that source cannot point to another :has_many :through association. So you could try the updated version.
class User < ActiveRecord::Base
#...
has_many :user_feed_events_of_friends_by_me, :through => :friends_by_me,
:source => :user_feed_events
has_many :feed_events_of_friends_by_me, :through => :user_feed_events_by_me
has_many :user_feed_events_of_friends_for_me, :through => :friends_for_me,
:source => :user_feed_events
has_many :feed_events_of_friends_for_me, :through => :user_feed_events_for_me
# A wrapper to return full list of two-way friendship events
def feeds_events_of_my_friends
self.feed_events_of_friends_by_me + self.feed_events_of_friends_for_me
end
end
Unfortunately the HMF plugin has two one-way friendship links, which means full list requires 2 DB queries.
I found a working and more traditional SQL solution:
friends_id = current_user.friends.collect {|f| f.id}.join(",")
sql = "SELECT feed_events.*, user_feed_events.user_id FROM feed_events LEFT JOIN user_feed_events ON feed_events.id = user_feed_events.feed_event_id WHERE user_feed_events.user_id IN (#{friends_id}) GROUP BY feed_events.id ORDER BY feed_events.created_at DESC"
friend_feed_events = FeedEvent.paginate_by_sql(sql, :page => params[:page], :per_page => 30)
If you have a more efficient / more elegant way of doing the same, please let me know!
Related
I've extended many of my has_many declarations to filter / join / preload associations. I'd like to re-use some of these extensions when I declare has_many :through relationships. Is this possible? Should I take a different approach?
Example:
I have this in my Library Model:
class Library < ActiveRecord::Base
has_many :meals, :dependent => :destroy do
def enabled
where(:enabled => true)
end
end
end
My Meal Model has this:
class Meal < ActiveRecord::Base
has_many :servings, :inverse_of => :meal, :dependent => :destroy
end
I'd like my library to have many servings, but only from the enabled meals. There are a couple ways I can do this:
# repeat the condition in the has_many :servings declaration
class Library < ActiveRecord::Base
has_many :servings, :through => :meals, :conditions => ["meals.enabled = ?", true]
end
# declare a different meals association for only the enabled meals
class Library < ActiveRecord::Base
has_many :enabled_meals, :class_name => "Meals", :conditions => [:enabled => true]
has_many :servings, :through => :enabled_meals
end
Is there any way to re-use the extension to my existing :meals declaration? (def enabled in the first code block)
Looks a lot like you want to use activerecord-association-extensions, as described here http://blog.zerosum.org/2007/2/8/activerecord-association-extensions.html.
I haven't tried it, but I think you could do:
module LibraryMealExtensions
def enabled?
where(:enabled=>true)
end
def standard_includes
includes(:servings)
end
end
class Library < ActiveRecord::Base
has_many :meals, :dependent => :destroy, :extend=>LibraryMealExtensions
has_many :servings, :through => :meals, :extend=>LibraryMealExtensions
end
Not sure about the "enabled=>true" there - you might have to say
where("meals.enabled=true")
b/c of confusion with aliases.
I have a Partner model that has_and_belongs_to_many Projects, while each Project has_many Sites. I want to retrieve all sites for a given partner (and am not interested in the projects in between at the moment).
I have accomplished what I need through a named_scope on the Site model, and a project.sites instance method that wraps a call to the Site named scope, as follows:
class Partner < ActiveRecord::Base
has_and_belongs_to_many :projects
def sites
Site.for_partner_id(self.id)
end
end
class Project < ActiveRecord::Base
has_many :sites
end
class Site < ActiveRecord::Base
belongs_to :project
named_scope :for_partner_id, lambda {|partner_id|
{ :include=>{:project=>:partners},
:conditions=>"partners.id = #{partner_id}"
}
}
end
Now, given a partner instance, I can call partner.sites and get back a collection of all sites associated with the partner. This is precisely the behavior I want, but I'm wondering if there's another way to do this using only activerecord associations, without the named scope?
I had a similar deep nesting query/collection problem here (I had to threaten to repeat data before anyone would answer my 4 questions, clever):
Is it appropriate to repeat data in models to satisfy using law of demeter in collections?
The trick is this gem http://rubygems.org/gems/nested_has_many_through which can do something like this:
class Author < User
has_many :posts
has_many :categories, :through => :posts, :uniq => true
has_many :similar_posts, :through => :categories, :source => :posts
has_many :similar_authors, :through => :similar_posts, :source => :author, :uniq => true
has_many :posts_of_similar_authors, :through => :similar_authors, :source => :posts, :uniq => true
has_many :commenters, :through => :posts, :uniq => true
end
class Post < ActiveRecord::Base
belongs_to :author
belongs_to :category
has_many :comments
has_many :commenters, :through => :comments, :source => :user, :uniq => true
end
This has super-simplified my queries and collections. I hope you find an answer to your problem, it's a tough one!
I have been trying to develop a movie based rails application which has support for multiple regions (Hollywood, Bollywood etc). I call the multiple regions as languages in the application.
Each language has its own set of data i.e., english has all the movies related to hollywood and language hindi has all the movies related to bollywood.
Language Model
class Language < ActiveRecord::Base
has_many :movies
has_many :cast_and_crews, :through => :movies, :uniq => true
has_many :celebrities, :through => :cast_and_crews, :uniq => true
# FIXME: Articles for celebrities and movies
has_many :article_associations, :through => :celebrities
has_many :articles, :through => :article_associations, :uniq => true
end
Here movies and celebrities both have articles using the article_association class.
Movie Model
class Movie < ActiveRecord::Base
belongs_to :language
has_many :cast_and_crews
has_many :celebrities, :through => :cast_and_crews
has_many :article_associations
has_many :articles, :through => :article_associations, :uniq => true
end
Celebrity Model
class Celebrity < ActiveRecord::Base
has_many :cast_and_crews
has_many :movies, :through => :cast_and_crews, :uniq => true
has_many :article_associations
has_many :articles, :through => :article_associations, :uniq => true
end
class ArticleAssociation < ActiveRecord::Base
belongs_to :article
belongs_to :celebrity
belongs_to :movie
end
and this is how my Article model is defined
class Article < ActiveRecord::Base
has_many :article_associations
has_many :celebrities, :through => :article_associations
has_many :movies, :through => :article_associations
end
What I am trying to achieve is language.article should return all the articles related to celebrities and movies.
The reason why I am not using SQL is find_by_sql does not support ActiveRelation and I will not be able use has_scope functionality.
I am using nested_has_many_through, has_scope and inherited_resources gems
Any help in this will be greatly appreciated.
Rails 3.1 now has support for nesting relations. Of course the built in one should be better then a plugin :)
http://railscasts.com/episodes/265-rails-3-1-overview
There are few tricks that should allow what you need, going out of Article you can query all the Moviesfor given language id
class Article < ActiveRecord::Base
has_many :article_associations
has_many :celebrities, :through => :article_associations
has_many :article_movies, :through => :article_associations, :class => 'Movie'
scope :for_language, lambda {|lang_id|
joins(
:article_associations=>[
:article_movies,
{:celebrities => { :cast_and_crews => :movies } }
]
).where(
'movies.language_id = ? OR article_movies.language_id = ?',
lang_id, lang_id
)
}
end
Then in language define a method that will use earlier scope of Article
class Language < ActiveRecord::Base
has_many :movies
has_many :cast_and_crews, :through => :movies, :uniq => true
has_many :celebrities, :through => :cast_and_crews, :uniq => true
def articles
Article.for_language id
end
end
The only unsure part here is how :article_movies will be represented in sql ...
ok This is what I did to fix this.
Added the following scope in my Article class
def self.region(region_id)
joins(<<-eos
INNER JOIN
(
SELECT DISTINCT aa.article_id
FROM regions r
LEFT JOIN movies m on m.region_id = r.id
LEFT JOIN cast_and_crews cc on cc.movie_id = m.id
LEFT JOIN celebrities c on c.id = cc.celebrity_id
LEFT JOIN events e on e.region_id = r.id
LEFT JOIN article_associations aa on (aa.event_id = e.id or aa.movie_id = m.id or aa.celebrity_id = c.id)
WHERE r.id = #{region_id}
) aa
eos
).where("aa.article_id = articles.id")
end
This gives me a ActiveRecord::Relation instance that I am expected which retrieves all the records for a movie, celebrity or event.
Thanks for all who helped me.
If you have any comments to improve it please comment it. Very much appreciated.
I'm trying to tidy up my code by using named_scopes in Rails 2.3.x but where I'm struggling with the has_many :through associations. I'm wondering if I'm putting the scopes in the wrong place...
Here's some pseudo code below. The problem is that the :accepted named scope is replicated twice... I could of course call :accepted something different but these are the statuses on the table and it seems wrong to call them something different. Can anyone shed light on whether I'm doing the following correctly or not?
I know Rails 3 is out but it's still in beta and it's a big project I'm doing so I can't use it in production yet.
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
named_scope :accepted, :conditions => ["connections.status = ?", Connection::ACCEPTED]
# the :accepted named_scope is duplicated
named_scope :accepted, :conditions => ["memberships.status = ?", Membership::ACCEPTED]
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
end
I'm trying to run something like person.contacts.accepted and group.members.accepted which are two different things. Shouldn't the named_scopes be in the Membership and Connection classes?
However if you try putting the named scopes in the Membership and Connection classes then you get this error (because Person.find(2).contacts returns an array of Persons which doesn't have an 'accepted' method:
>> Person.find(2).contacts.accepted
NoMethodError: undefined method `accepted' for #<Class:0x108641f28>
One solution is to just call the two different named scope something different in the Person class or even to create separate associations (ie. has_many :accepted_members and has_many :accepted_contacts) but it seems hackish and in reality I have many more than just accepted (ie. banned members, ignored connections, pending, requested etc etc)
You answered your own question:
Shouldn't the named_scopes be in the Membership and Connection classes?
Yes, they should be. This will let you call them as you wanted. It's also logically where they belong.
If you want something on Person that checks both, you can do something like:
named_scope :accepted, :conditions => ["connections.status = ? OR memberships.status = ?", Connection::ACCEPTED, Membership::ACCEPTED]
Maybe you want this to be an AND? not sure.
I'm sure this is not the best way and I believe you can do this on person and group models, but I also believe the following will work for you:
# models
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
has_many :memberships
has_many :groups, :through => :memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
named_scope :accepted, :conditions => ["status = ?", Connection::ACCEPTED]
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
named_scope :accepted, :conditions => ["status = ?", Membership::ACCEPTED]
end
# controller
# get person's accepted contacts
#person = Person.first
#person.connections.accepted.map(&:contact)
# get group's accepted members
#group = Group.first
#group.memberships.accepted.map(&:person)
Can't wrap my head around this...
class User < ActiveRecord::Base
has_many :fantasies, :through => :fantasizings
has_many :fantasizings, :dependent => :destroy
end
class Fantasy < ActiveRecord::Base
has_many :users, :through => :fantasizings
has_many :fantasizings, :dependent => :destroy
end
class Fantasizing < ActiveRecord::Base
belongs_to :user
belongs_to :fantasy
end
... which works fine for my primary relationship, in that a User can have many Fantasies, and that a Fantasy can belong to many Users.
However, I need to add another relationship for liking (as in, a User "likes" a Fantasy rather than "has" it... think of Facebook and how you can "like" a wall-post, even though it doesn't "belong" to you... in fact, the Facebook example is almost exactly what I'm aiming for).
I gathered that I should make another association, but I'm kinda confused as to how I might use it, or if this is even the right approach. I started by adding the following:
class Fantasy < ActiveRecord::Base
...
has_many :users, :through => :approvals
has_many :approvals, :dependent => :destroy
end
class User < ActiveRecord::Base
...
has_many :fantasies, :through => :approvals
has_many :approvals, :dependent => :destroy
end
class Approval < ActiveRecord::Base
belongs_to :user
belongs_to :fantasy
end
... but how do I create the association through Approval rather than through Fantasizing?
If someone could set me straight on this, I'd be much obliged!
Keep your first set of code, then in your User Model add:
has_many :approved_fantasies, :through => :fantasizings, :source => :fantasy, :conditions => "fantasizings.is_approved = 1"
In your Fantasizing table, add an is_approved boolean field.