Rails Model has_many with multiple foreign_keys - ruby-on-rails

Relatively new to rails and trying to model a very simple family "tree" with a single Person model that has a name, gender, father_id and mother_id (2 parents). Below is basically what I want to do, but obviously I can't repeat the :children in a has_many (the first gets overwritten).
class Person < ActiveRecord::Base
belongs_to :father, :class_name => 'Person'
belongs_to :mother, :class_name => 'Person'
has_many :children, :class_name => 'Person', :foreign_key => 'mother_id'
has_many :children, :class_name => 'Person', :foreign_key => 'father_id'
end
Is there a simple way to use has_many with 2 foreign keys, or maybe change the foreign key based on the object's gender? Or is there another/better way altogether?
Thanks!

Found a simple answer on IRC that seems to work (thanks to Radar):
class Person < ActiveRecord::Base
belongs_to :father, :class_name => 'Person'
belongs_to :mother, :class_name => 'Person'
has_many :children_of_father, :class_name => 'Person', :foreign_key => 'father_id'
has_many :children_of_mother, :class_name => 'Person', :foreign_key => 'mother_id'
def children
children_of_mother + children_of_father
end
end

To improve on Kenzie's answer, you can achieve an ActiveRecord Relation by defining Person#children like:
def children
children_of_mother.merge(children_of_father)
end
see this answer for more details

Used named_scopes over the Person model
do this:
class Person < ActiveRecord::Base
def children
Person.with_parent(id)
end
named_scope :with_parent, lambda{ |pid|
{ :conditions=>["father_id = ? or mother_id=?", pid, pid]}
}
end

I believe you can achieve the relationships you want using :has_one.
class Person < ActiveRecord::Base
has_one :father, :class_name => 'Person', :foreign_key => 'father_id'
has_one :mother, :class_name => 'Person', :foreign_key => 'mother_id'
has_many :children, :class_name => 'Person'
end
I'll confirm and edit this answer after work ; )

My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
class Person < ActiveRecord::Base
belongs_to :father, :class_name => 'Person'
belongs_to :mother, :class_name => 'Person'
has_many :children, ->(person) { unscope(where: :person_id).where("father_id = ? OR mother_id = ?", person.id, person.id) }, class_name: 'Person'
end
So any questions?

I prefer to use scopes for this issue. Like this:
class Person < ActiveRecord::Base
belongs_to :father, :class_name => 'Person'
belongs_to :mother, :class_name => 'Person'
has_many :children_of_father, :class_name => 'Person', :foreign_key => 'father_id'
has_many :children_of_mother, :class_name => 'Person', :foreign_key => 'mother_id'
scope :children_for, lambda {|father_id, mother_id| where('father_id = ? AND mother_id = ?', father_id, mother_id) }
end
This trick make it easy to get children without use instances:
Person.children_for father_id, mother_id

Not a solution to the general question as stated ("has_many with multiple foreign keys"), but, given a person can either be a mother or a father, but not both, I would add a gender column and go with
has_many :children_of_father, :class_name => 'Person', :foreign_key => 'father_id'
has_many :children_of_mother, :class_name => 'Person', :foreign_key => 'mother_id'
def children
gender == "male" ? children_of_father : children_of_mother
end

I was looking for the same feature, if you don't want to return an array but a ActiveRecord::AssociationRelation, you can use << instead of +.
(See the ActiveRecord documentation)
class Person < ActiveRecord::Base
belongs_to :father, :class_name => 'Person'
belongs_to :mother, :class_name => 'Person'
has_many :children_of_father, :class_name => 'Person', :foreign_key => 'father_id'
has_many :children_of_mother, :class_name => 'Person', :foreign_key => 'mother_id'
def children
children_of_mother << children_of_father
end
end

Related

How to << a Join Model with attributes to a collection

I have a User model which joins to another User model through a UserRelationship join table. UserRelationships have an attribute (approved/pending/revoked) which must be set, but does not default to any option. There are two associations which reflect this, traveler and delegate, so my models look like this:
User.rb
has_many :traveler_relationships, :class_name => 'UserRelationship', :foreign_key => :delegate_id
has_many :travelers, :class_name => 'User', :through => :traveler_relationships
has_many :delegate_relationships, :class_name => 'UserRelationship', :foreign_key => :user_id
has_many :delegates, :class_name => 'User', :through => :delegate_relationships
has_many :buddy_relationships, class_name: 'UserRelationship', foreign_key: :user_id
has_many :buddies, class_name: 'User', through: :buddy_relationships, source: :delegate
UserRelationship.rb
belongs_to :relationship_status
belongs_to :traveler, :class_name => 'User', :foreign_key => 'user_id'
belongs_to :delegate, :class_name => 'User'
PENDING = 1
CONFIRMED = 3
REVOKED = 5
I'm trying to write up some specs where one user is related to another and the simplest way to write it would be #user1.travelers << #user2 but this fails the database constraint that UserRelationship.relationship_status not be null.
When I try #user1.buddies.create(delegate: #user2, relationship_status: RelationshipStatus::CONFIRMED), it fails saying UnknownAttributeError on delegate. I looked at this question and tried its solution, using attr_acessible, but it didn't change the UnknownAttributeError.
What is the way to create this join record with an attribute set?
I assume UserRelationship has a foreign_key delegate_id for the delegate
class User < ActiveRecord::Base
has_many :traveler_relationships, :class_name => 'UserRelationship', :foreign_key => :user_id
has_many :delegate_relationships, :class_name => 'UserRelationship', :foreign_key => :delegate_id
has_many :travelers, :class_name => 'User', :through => :traveler_relationships
has_many :delegates, :class_name => 'User', :through => :delegate_relationships
end
class UserRelationship < ActiveRecord::Base
belongs_to :traveler, :class_name => 'User', :foreign_key => :user_id
belongs_to :delegate, :class_name => 'User', :foreign_key => :delegate_id
PENDING = 1
CONFIRMED = 3
REVOKED = 5
end
I dont think you can use the shortcut #user1.delegates << #user2 since you need to specify relationship status. Try:
#user1.traveler_relationships.create(delegate: #user2, relationship_status: RelationshipStatus::CONFIRMED)
I've excluded the buddy synonym here. It's complex enough as is. When this works you can look into adding synonyms.
You're on the right track when you try to create from buddies (I'm assuming that's your join model relation). The problem is that your column name is relationship_status and you were passing status to create. Try this:
#user1.buddies.create(delegate: #user2, relationship_status: RelationshipStatus::CONFIRMED

has_one relationship to the same class

I have the following Person class that has a parent that is also another Person. I can't seem to figure out how to get the relations to work.
class Person < ActiveRecord::Base
attr_accessible :mom, :dad
has_one :mom, :class_name => 'Person', :primary_key => "mom_id", :foreign_key => "id"
has_one :dad, :class_name => 'Person', :primary_key => "dad_id", :foreign_key => "id"
end
I have added "mom_id" and "dad_id" as integers to my model with a migration. However, when I use the rails console, I'm not able to access mom or dad attributes after settings the mom_id and dad_id. They still return nil.
Any pointers to what I am doing wrong?
I agree with #Andrew, this should be belongs_to
belongs_to :mom, :class_name => "Person", :foreign_key => "mom_id"
belongs_to :dad, :class_name => "Person", :foreign_key => "dad_id"

ActionRecord modeling: Employee with many managers

This should be easy. I think I must be getting caught up on naming.
Both a 'manager' and a 'subordinate' (employee) are of class "Person".
Here's what I have:
class Person < ActiveRecord::Base
has_many :person_manager_assignments
has_many :managers, :through => :person_manager_assignments
has_many :subordinates, :through => :person_manager_assignments
end
class PersonManagerAssignment < ActiveRecord::Base
has_one :subordinate, :class_name => "Person", :foreign_key => "id", :primary_key => 'person_id'
has_one :manager, :class_name => "Person", :foreign_key => "id", :primary_key => 'manager_id'
end
Which works great for checking and assigning managers.
I'm caught on the part about subordinates. It returns the Person's self, instead of their subordinates:
p.subordinates
Person Load (0.5ms) SELECT "people".* FROM "people" INNER JOIN "person_manager_assignments" ON "people"."id" = "person_manager_assignments"."person_id" WHERE "person_manager_assignments"."person_id" = 15973
See the bit where in the WHERE clause where it's matching "person_id"? I need that to be "manager_id", but messing with the PersonManagerAssignment associations foreign_key and primary_key values doesn't seem to help.
Any ideas?
Answer is essentially here: http://railscasts.com/episodes/163-self-referential-association
So I think you need this:
class Person < ActiveRecord::Base
has_many :person_manager_assignments
has_many :managers, :through => :person_manager_assignments
has_many :subordinate_relationships, :class_name=>"PersonManagerAssignment", :foreign_key=>"manager_id"
has_many :subordinates, :through => :subordinate_relationships, :source=>:person
end
and
class PersonManagerAssignment < ActiveRecord::Base
belongs_to :person
belongs_to :manager, :class_name=>"Person"
end
Rock on.
I'm going to guess that your PersonManagerAssignment table has a person_id and a manager_id and associations in the model like has_one :person and has_one :manager. If that's the case, I'll recommend changing this association
has_one :person
to this
has_one :subordinate, :class_name => "Person", :foreign_key => "person_id"
and then your has_many :subordinates should work as expected.
class Person < ActiveRecord::Base
has_many :subordinates :through => :person_manager_assignments
has_many :managers, :through => :person_manager_assignments
end
class PersonManagerAssignment < ActiveRecord::Base
belongs_to :subordinate, :class_name => 'Person'
belongs_to :manager, :class_name => 'Person'
end

How can I has_and_belongs_to_many multiple instances of the same model?

Basically, I want to accomplish something like this:
Class Node < ActiveRecord::Base
has_and_belongs_to_many :parents, :class_name=>'Node'
has_and_belongs_to_many :children, :class_name=>'Node'
end
but it isn't working, and I'm not entirely sure the proper way to do this. I'm going to try explicitly defining a join table next and have both use it: If that's the solution, would the column be called "children_id" or "child_id"?
Class Node < ActiveRecord::Base
has_and_belongs_to_many :parents, :class_name=>'Node', :join_table => "parents_children", :foreign_key => :child_id, :association_foreign_key => :parent_id
has_and_belongs_to_many :children, :class_name=>'Node', :join_table => "parents_children", :foreign_key => :parent_id, :association_foreign_key => :child_id
end
Note that you can rename the join table and foreign keys as long as you set the appropriate foreign key names here.
This is doable, but I strongly recommend using has_many :through instead:
class Node < ActiveRecord::Base
has_many :parent_node_links,
:class_name => 'NodeLink',
:foreign_key => :child_id
has_many :parents,
:through => :parent_node_links,
:source => :parent
has_many :child_node_links,
:class_name => 'NodeLink',
:foreign_key => :parent_id
has_many :children,
:through => :child_node_links,
:source => :child
end
class NodeLink < ActiveRecord::Base
belongs_to :parent,
:class_name => "Node"
belongs_to :child,
:class_name => "Node"
end
Having a first-class join model makes it much easier to manage the relationships and gives you the freedom to add relevant meta-data at a later point in time.

rails way user to user messaging, do I need a join table?

Quick question (I think). I have users, and I would like to allow them to send messages to one another. My question is, should I have a user table, a message table, and a users to messages join table, or should I just store to_user_id and from_user_id in the messages table. If the latter, what would the association look like for that? Can you even reference a 'local' key for an association?
You can do that with a couple of simple has_many associations. Since it's self-referential, you'll need to override some of the Rails magic to make it work.
class User < ActiveRecord::Base
has_many :sent_messages, :class_name => 'Message', :foreign_key => 'sender_id'
has_many :received_messages, :class_name => 'Message', :foreign_key => 'recipient_id'
end
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => 'User'
belongs_to :recipient, :class_name => 'User'
end
Rails doesn't have a cleaner way to doing self-referential associations that I know of.
I think the latter sounds fine. This is just off the top of my head but I know AR's associations have options like this...
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => :user, :foreign_key => :from_user_id
belongs_to :recipient, :class_name => :user, :foreign_key => :to_user_id
#...
end
class User < ActiveRecord::Base
has_many :received_messages, :class_name => :message, :foreign_key => :to_user_id
has_many :sent_messages, :class_name => :message, :foreign_key => :from_user_id
end

Resources