Rails 3 complex associations using nested_has_many_through - ruby-on-rails

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.

Related

How to add new relation in many to many relations

I want to create lists that are edited by many users. So I go with many to many relations like this.
class User < ActiveRecord::Base
has_many :editabilities, :dependent => :destroy
has_many :editable_lists, :through => :editabilities, :source => :list
end
class List < ActiveRecord::Base
has_many :editabilities, :dependent => :destroy
has_many :editors, :through => :editabilities, :source => :user
end
class Editability < ActiveRecord::Base
belongs_to :list
belongs_to :user
end
And I add a editor for a list by like this.
u = User.first
l = List.first
Editability.create(user_id: u.id, list_id: l.id)
It seems to be working. But I'm not sure whether I am doing in a right way or wrong way. Is this a right way to do?
Also:
u = User.first
l = List.first
u.editable_lists << l
http://guides.rubyonrails.org/association_basics.html#has-many-association-reference

In rails 2.3, how can I retrieve a collection of objects from a second-order has_many association?

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!

Joining friends to other models

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!

access has_many relation in two different ways ActiveRecord Rails

I need to access institution in two ways.
My models are given below:
class Person < ActiveRecord::Base
has_many :institution_people
has_many :institution_choices
has_many :institutions, :through => :institution_people
has_many :institutions, :through => :institution_choices
fields........
end
class Institution < ActiveRecord::Base
has_many :people, :through => :institution_people
has_many :people, :through => :institution_choices
has_many :institution_people
has_many :institution_choices
end
class InstitutionChoice < ActiveRecord::Base
belongs_to :person
belongs_to :institution
end
class InstitutionPerson < ActiveRecord::Base
belongs_to :person
belongs_to :institution
end
The i setup the models like this is that person can study in different institutions, so for this i setup
has_many :institutions, :through => :institution_people
for person model
But at the same time person can have institution choices, so i setup
has_many :institutions, :through => :institution_choices
for person model.
How should i setup model and association between person and institutions so that i can find institutions from person in both ways.
Right now
Person.first.institutions
finds from institution_people table, as
has_many :institutions, :through => :institution_people
is at beginning i guess.
Some other techniques are welcomed so that i can get institutions in both ways.
In your Person model, try this :
class Person < ActiveRecord::Base
has_many :institution_people
has_many :institution_choices
has_many :institutions_people, :through => :institution_people, :source => :institutions, :class_name => "Institution"
has_many :institutions_choices, :through => :institution_choices, :source => :institutions, :class_name => "Institution"
end
http://guides.rubyonrails.org/association_basics.html#has_many-association-reference
Basically, you need some kind of interface here. What i would do:
in institutions model:
scope :institutions_of, proc { |person| joins(' INNER JOIN (' + Person.institution_ids(person) + ') q
ON institutions.id = q.ip_iid OR institutions.id = q.ic_iid').where(['institutions.person_id = ?', person_id]
( it joins the query(scope) from Person.rb below )
in Person.rb :
scope :institution_ids, proc { |person| select('ip.institution_id ip_idd, ic.institution_id ic_idd
from institution_people ip
inner join institution_choice ic on ip.person_id = ic.person_id').
where(['ip.person_id = ?', person.id])
(this should retrieve all institutions ids from both tables)
ugly as hell, but still might work. You could use is then like: Institution.institutions_of(current_user)
If you don't need to keep the result as an ActiveRelation
class Person < ActiveRecord::Base
has_many :institution_people
has_many :institution_choices
def institutions
institution_people + institution_choices
end
end
If you DO need the keep it (in order to call person.institutions.order('name'), by example), then it's more complicated:
class Person < ActiveRecord::Base
has_many :institution_people
has_many :institution_choices
scope :institutions, :select => 'select * from (select * from institution_people union all select * from institution_choices) as institutions'
end

has_many through self referential association

I want to (as an example) create a has_many association to all posts by friends of a person, something like has_many :remote_posts to give me something like person > friends > person > posts.
..here is how I would go about it
script/generate model post title:string person_id:integer
script/generate model friendship person_id:integer friend_id:integer
script/generate model person name:string
class Person < ActiveRecord::Base
has_many :posts
has_many :friendships, :foreign_key => 'friend_id'
has_many :people, :through => :friendships
has_many :remote_posts, :class_name => 'Post', :through => :people, :source => :posts
end
class Friendship < ActiveRecord::Base
belongs_to :person
#also has a 'friend_id' to see who the friendship is aimed at
end
class Post < ActiveRecord::Base
belongs_to :person
end
# generate some people and friends
{'frank' => ['bob','phil'], 'bob' => ['phil']}.each {|k,v|
v.each {|f|
Friendship.create(
:person_id => Person.find_or_create_by_name(f).id,
:friend_id => Person.find_or_create_by_name(k).id
)
}
}
# generate some posts
Person.all.each {|p|
p.posts.create({:title => "Post by #{p.name}"})
}
Now,
Person.first.friendships # ..works
Person.first.people # (friends) ..works
Person.first.posts # ..works
Person.first.remote_posts #....
...and I get this error..
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: people.person_id: SELECT "posts".* FROM "posts" INNER JOIN "people" ON "posts".person_id = "people".id WHERE (("people".person_id = 1))
Aside from the foreign key error - seems like the friendships association isn't coming into play at all. I was thinking that this might be because of the :source => :posts, since the posts association would come into it twice.
I could write some finder sql (and that is what I have working at the moment), though I'd sooner do it this way.
Any ideas of how to get this to work?
How about this:
In the FriendShip class, add:
has_many :posts, :through => :person
and in the Person class, change the remote_posts to:
has_many :remote_posts, :class_name => 'Post',
:through => :friendships, :source => :person
How about a nested has_many :through relationship. This seems to work for me:
class Friendship < ActiveRecord::Base
belongs_to :person
belongs_to :friend, :class_name => 'Person'
has_many :posts, :through => :friend, :source => :posts
end
class Person < ActiveRecord::Base
has_many :posts
has_many :friendships, :foreign_key => 'friend_id'
has_many :people, :through => :friendships
has_many :remote_posts, :through => :friendships, :source => :posts
end
Note: this requires this nested_has_many_through plugin. (Note: direct linking to github repos seems to be broken... but that repo is there despite the error message.)

Resources