I have two models: Users and Projects. The idea is that Users can follow both projects AND other users. Naturally, Users and Projects are part of a polymorphic "followable" type. Now, using the user model, I'd like to get three things:
user.followed_users
user.followed_projects
user.followers
The first two work fine; It's the third that I'm having trouble with. This is sort of a reverse lookup where the foreign key becomes the "followable_id" column in the follows table, but no matter how I model it, I can't get the query to run correctly.
User Model
has_many :follows, :dependent => :destroy
has_many :followed_projects, :through => :follows, :source => :followable, :source_type => "Project"
has_many :followed_users, :through => :follows, :source => :followable, :source_type => "User"
has_many :followers, :through => :follows, :as => :followable, :foreign_key => "followable", :source => :user, :class_name => "User"
Follow Model
class Follow < ActiveRecord::Base
belongs_to :followable, :polymorphic => true
belongs_to :user
end
My follows table has:
user_id
followable_id
followable_type
Whenever I run the query I get:
SELECT `users`.* FROM `users` INNER JOIN `follows` ON `users`.`id` = `follows`.`user_id` WHERE `follows`.`user_id` = 7
where it should be "followable_id = 7 AND followable_type = 'User", not "user_id = 7"
Any thoughts?
Figured it out. Took a look at a sample project Michael Hartl made and noticed that the correct way to do this is to specify not only a relationship table (in this case follows) but also a reverse relationship table (which I called reverse follows).
has_many :follows,
:dependent => :destroy
has_many :followed_projects,
:through => :follows,
:source => :followable,
:source_type => "Project"
has_many :followed_users,
:through => :follows,
:source => :followable,
:source_type => "User"
has_many :reverse_follows,
:as => :followable,
:foreign_key => :followable_id,
:class_name => "Follow"
has_many :followers,
:through => :reverse_follows,
:source => :user
Hope this helps some people out down the line!
I think you need to explicitly spell out the foreign key. You have:
:foreign_key => "followable"
you need:
:foreign_key => "followable_id"
full code:
has_many :followers, :through => :follows, :as => :followable, :foreign_key => "followable_id", :source => :user, :class_name => "User"
Related
A User has many subscribers and many publishers, both of which are users:
class User < ActiveRecord::Base
has_many :relationships, :foreign_key => "subscriber_id"
has_many :subscribers, :through => :relationships, :source => :subscriber
has_many :inverse_relationships, :class_name => "Relationship", :foreign_key => "publisher_id"
has_many :publishers, :through => :inverse_relationships, :source => :publisher
def subscribe_to(publisher)
self.relationships.create!(publisher_id: publisher.id, subscriber_id: id)
end
end
class Relationship < ActiveRecord::Base
belongs_to :subscriber, :class_name => "User"
belongs_to :publisher, :class_name => "User"
end
A user's publishers is who the user is subscribed to.
However, if I do john.subscribe_to(a_publisher), and then attempt to puts john.publishers, I get back an empty array.
I'm going back and forth randomly changing the foreign key and the source, hoping that it will eventually work, but something is off. What should the sources and foreign keys be here?
Update
Here's what I did to make it work:
class User < ActiveRecord::Base
has_many :relationships, :foreign_key => "publisher_id"
has_many :subscribers, :through => :relationships, :source => :subscriber
has_many :inverse_relationships, :foreign_key => "subscriber_id", :class_name => "Relationship"
has_many :publishers, :through => :inverse_relationships, :source => :publisher
def subscribe_to(publisher)
publisher.relationships.create!(subscriber_id: id)
end
end
First, I switched the foreign keys for both. Second, and this is the part I don't understand, I changed
self.relationships.create!(publisher_id: publisher.id)
to
publisher.relationships.create!(subscriber_id: id)
and it worked. For some reason, it doesn't work the other way around. Can anyone explain why?
Your association seems strange. You should try like this:
has_many :subscribers, :through => :relationships, :class_name => 'User'
And call :
john.subscribers.create(a_publisher)
Nowhere you're saying that subscriber is a User. I think your problem comes from here.
Maybe you can try with: :source => :user
I'm setting up a Rails project for a client, and they want Users (a model) to be able to follow each other (as in Twitter). They also want to be able to keep track of when one user started following another.
Since I need to keep track of the creation date, I figured, a has_many X, :through => Y relation would be the way to go, so the Y will keep track of the date it was created.
I have my Follow model set up:
class Follow < ActiveRecord::Base
attr_accessible :actor_id, :observer_id, :follower, :followee
attr_readonly :actor_id, :observer_id, :follower, :followee
belongs_to :follower, :class_name => 'User', :foreign_key => :observer_id
belongs_to :followee, :class_name => 'User', :foreign_key => :actor_id
validates_presence_of :follower, :followee
validates_uniqueness_of :actor_id, :scope => :observer_id
end
The question is how do I set up the relations in the User model?
Ideally I'd like it to have the following:
:follows would be the associated Follow objects where self is the follower (observer_id)
:followed would be the associated Follow objects where self is the followee (actor_id)
:following would be the associated User objects where self is the follower (observer_id)
:followers would be the associated User objects where self is the followee (actor_id)
I'm not sure how to write the has_many :through parts, though? Should I be using :source => X or foreign_key => X? And which key (actor_id or observer_id) should I put in each?
Edit: I'm currently doing this
has_many :follows, :foreign_key => :observer_id
has_many :followed, :class_name => 'Follow', :foreign_key => :actor_id
has_many :following, :class_name => 'User', :through => :follows, :source => :followee, :uniq => true
has_many :followers, :class_name => 'User', :through => :follows, :source => :follower, :uniq => true
and it's mostly working. All of them except :followers work fine, but user.followers is doing something weird. It seems like it's checking whether user is followING someone, and if they are then user.followers returns an array containing only user; if they're not, it returns an empty array.
Does anyone have any advice?
It looks like this is the correct format:
has_many :follows, :foreign_key => :observer_id
has_many :followed, :class_name => 'Follow', :foreign_key => :actor_id
has_many :following, :class_name => 'User', :through => :follows, :source => :followee, :uniq => true
has_many :followers, :class_name => 'User', :through => :followed, :source => :follower, :uniq => true
For Rails newbies, the :uniq => true is important (as I discovered while doing this) because it keeps has_many X, :through => Y relations from returning duplicates (that is, without it, you might get multiple distinct objects, each with their own object ID, all referring to the same record/row).
I have a relationship that is as follows.
companies_employee.rb
belongs_to :employee
belongs_to :company
validates_presence_of :role
employee.rb
has_many :companies_employees
has_many :companies, :through => :companies_employees
company.rb
has_many :companies_employees
has_many :managers, :through => :companies_employees, :source => :employee, conditions => {:role => "Manager"}
has_many :owners, :through => :companies_employees, :source => :employee, :conditions => {:role => "Owner"}
My problem is that when it checks the conditions, it tries to find the role column in the employees table, but the role column is in the companies_employees table.
Is there a way to make it use things in this table for the conditions?
Try something like this:
has_many :managers, :through => :companies_employees, :source => :employee, conditions => ["employees.role = 'Manager"]
has_many :owners, :through => :companies_employees, :source => :employee, conditions => ["employees.role = 'Owner"]
Let's assume I have an application with a "favorites" feature, where users can add a document, note, or comment to his favorites list.
In my mind..
User has_many Favorites
Favorite belongs_to a User
Document belongs_to a Favorite
Note belongs_to a Favorite
Comment belongs_to a Favorite
What's the problem with this type of association and how would polymorphic associations help?
because then your Favorite instance will not know what it is favouriting :)
it knows that it has_one :note, but also has_one :comment, or? but not both surely.
polymorphic association the opposite way helps because it will express that a Favorite object belongs_to :favorited object that is polymorphic cos it can be any class the name of which will be stored in the :favorited_type string db column, so your favorite object will know that it favors a note or document or comment.
with some code
class Note
has_many :favorites, :as => :favorited
has_many :fans, :through => :favorites, :source => :user
end
class Discussion
has_many :favorites, :as => :favorited
has_many :fans, :through => :favorites, :source => :user
end
class Comment
has_many :favorites, :as => :favorited
has_many :fans, :through => :favorites, :source => :user
end
class Favorite
belongs_to :user
belongs_to :favorited, :polymorphic => true # note direction of polymorphy
end
class User
has_many :favorites
has_many :favorite_notes, :through => :favorites, :source => favorited, :source_type => "Note"
has_many :favorite_comments, :through => :favorites, :source => favorited, :source_type => "Comment"
has_many :favorite_discussions, :through => :favorites, :source => favorited, :source_type => "Discussion"
end
(just set up your db correctly) this design is standard for such usecase of favoriting.
I got some Manager and SoccerTeam model. A manager "owns" many soccer teams; also a manager can comment on soccer teams and can comment on other managers too:
manager.rb
# Soccer teams the manager owns
has_many :soccer_teams, :dependent => :restrict
# Comments the manager has made on soccer teams or other managers
has_many :reviews, :class_name => "Comment", :foreign_key => :author_id, :dependent => :destroy
# Comments the manager has received by other managers
has_many :comments, :as => :commentable, :dependent => :destroy
# Soccer teams that have received a comment by the manager
has_many :observed_teams, :through => :comments, :source => :commentable, :source_type => "SoccerTeam"
soccer_team.rb
# The manager that owns the team
belongs_to :manager
# Comments received by managers
has_many :comments, :as => :commentable, :dependent => :destroy
# Managers that have reviewed the team
has_many :observers, :through => :comments, :source => :author, :class_name => "Manager"
comment.rb
belongs_to :commentable, :polymorphic => true
belongs_to :author, :class_name => Manager
Now, if I have a Manager commenting on a SoccerTeam I expect to find:
A Comment object in manager.reviews and in soccer_team.comments
A SoccerTeam object in manager.observed_teams
A Manager object in soccer_team.observers
While everything works well for the first and the third point, when I call manager.observed_teams I always obtain an empty array. To actually obtain the list of soccer teams a manager has commented on I need to use:
manager.reviews.collect{ |review| Kernel.const_get(review.commentable_type).find(review.commentable_id) if review.commentable_type == "SoccerTeam" }
Which is ugly. I expect the simple manager.observed_teams to work… why it doesn't?
Edit
I went a step further in understanding why it does not work. In fact, the genrated SQL is:
SELECT "soccer_teams".* FROM "soccer_teams" INNER JOIN "comments" ON "soccer_teams".id = "soccer_teams".commentable_id AND "comments".commentable_type = 'SoccerTeam' WHERE (("comments".commentable_id = 1) AND ("comments".commentable_type = 'Manager'))
While I want it to be:
SELECT "soccer_teams".* FROM "soccer_teams" INNER JOIN "comments" ON "soccer_teams".id = "comments".commentable_id AND "comments".commentable_type = 'SoccerTeam' WHERE ("comments".author_id = 1)
So the question is simple: how to obtain that query? (An heuristic attempt with :foreign_key ans :as, as expected, hasn't solved the problem!).
I think you've simply used the wrong association for observed_teams. Instead of
has_many :observed_teams, :through => :comments,
:source => :commentable, :source_type => "SoccerTeam"
Try this:
has_many :observed_teams, :through => :reviews,
:source => :commentable, :source_type => "SoccerTeam"
Also in,
has_many :reviews, :class_name => :comment,
:foreign_key => :author_id, :dependent => :destroy
:comment should be 'Comment'
and in
has_many :comments, :as => commentable, :dependent => :destroy
commmentable should be :commmentable