Self-referential has_many :through relationship


#1

Hi,
I have a self-referential has_many :through relationship setup to
track relationships between users. Basically relationships are
modeled as a join table with an extra column ‘relation’.

create table relationships (
user_id integer unsigned not null,
friend_id integer unsigned not null,
relation char(1) not null,
)

— relations —
f = friend
r = request to be a friend
b = blocked

My models look like this:

class Relationship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => ‘User’, :foreign_key => ‘friend_id’
end

class User < ActiveRecord::Base
has_many :relationships

has_many :friendships, :class_name => ‘Relationship’, :foreign_key
=> ‘user_id’, :conditions => “relation = ‘f’”
has_many :potential_friendships, :class_name => ‘Relationship’,
:foreign_key => ‘user_id’, :conditions => “relation = ‘r’”

has_many :friends, :through => :friendships
has_many :potential_friends, :through => :potential_friendships
end

Everything works great except the last potential_friends line. It
seems unable to figure out to use the friend_id foreign key. I have
tried to specified the :source param according to the docs. Am I
missing something here? The exact error is:

User.find(1).potential_friends
ActiveRecord::HasManyThroughSourceAssociationNotFoundError:
ActiveRecord::HasManyThroughSourceAssociationNotFoundError
from
/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/reflection.rb:173:in
check_validity!' from /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/associations/has_many_through_association.rb:6:ininitialize’
from
/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/associations.rb:876:in
`potential_friends’
from (irb):4

By the way I have read the excellent article over at
http://blog.hasmanythrough.com/articles/2006/04/21/self-referential-through.
Any ideas?

Thanks,
Zack

u.potential_friendships
=> [#<Relationship:0x247d44c @attributes={“relation”=>“r”,
“user_id”=>“1”, “friend_id”=>“22”}>, #<Relationship:0x247d410
@attributes={“relation”=>“r”, “user_id”=>“1”, “friend_id”=>“23”}>]

u.potential_friends
ActiveRecord::HasManyThroughSourceAssociationNotFoundError:
ActiveRecord::HasManyThroughSourceAssociationNotFoundError
from
/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/reflection.rb:173:in
check_validity!' from /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/associations/has_many_through_association.rb:6:ininitialize’
from
/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/associations.rb:876:in
`potential_friends’
from (irb):4

--------------- [ schema, classes] ---------------

create table relationships (
user_id integer unsigned not null,
friend_id integer unsigned not null,
relation char(1) not null,
foreign key (user_id) references users(id) on update cascade on
delete cascade,
foreign key (friend_id) references users(id) on update cascade on
delete cascade,
primary key(user_id, friend_id)
) engine=innodb character set utf8;

class Relationship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => ‘User’, :foreign_key => ‘friend_id’
end

class User < ActiveRecord::Base
has_many :relationships

has_many :friendships, :class_name => ‘Relationship’, :foreign_key
=> ‘user_id’, :conditions => “relation = ‘f’”
has_many :potential_friendships, :class_name => ‘Relationship’,
:foreign_key => ‘user_id’, :conditions => “relation = ‘r’”

has_many :friends, :through => :friendships
has_many :potential_friends, :through => :potential_friendships
end


#2

This might help:

http://blog.hasmanythrough.com/articles/2006/04/21/self-referential-through


#3

Thanks for the help - but if you re-read my post I mention that I’ve
already read this excellent blog entry (which helped me get everything
working except this one relationship).

Any other thoughts??

Zack


#4

oops. sorry. I kinda skimmed over it. Sorry I can’t be of any help.
-N


#5

Hi Zack,

Glad to see you making use of my blog. It looks like you’re on the
right track, but I think your model has a couple problems. Don’t worry
about it, as this is one of the trickier relationships to model.

The tricky part is that the join model table embodies friendship
relationships in two directions. In the English language friendship is
symmetrical, unlike a parent/child relationship. So you have to invent
the concepts of a friender and friendee. Call them whatever you like,
but you need to be able to distinguish a person who considers someone
else a friend from the someone who is considered a friend by that
person. That gives you have two ways for each person to be related by
friendship: 1) considering someone a friend, and 2) being considered a
friend. Those two relationships are what you need to model.

To model each relationship, you need a separate path through the join
model, each with a different foreign key. Look again at the example on
my blog and set something up that is equivalent to that. I suggest
starting simple and leaving out the different associations for actual
friends and potential friends. Once you get the basics working so that
each person can find his friends ane the people who call him a friend,
then you can work on potential friendships (in both directions, right?).
By the way, the conditions you are using for potential friendships look
fine, you just need to sort out the basic structure of the associations.


Josh S.
http://blog.hasmanythrough.com


#6

Josh,

Thanks for the response. Your blog has been a lot of help to clear up
the sometimes tricky has_many through associations. I understand what
you mean when you say the “join model table embodies friendship
relationships in two directions.” However in this case I only care
about it in one direction. In fact all the associations work fine in
my model except for the pontential_friends one. From
./script/console:

User.find(1).relationships # works fine
User.find(1).friendships # works fine
User.find(1).potential_friendships # works fine
User.find(1).friends # works fine
User.find(1).potential_friends # crashes

I’m thinking that AR is unable to reflect on potential_friends and
determine to use the friend_id foreign key. I’m normally able to poke
through the rails source fairly well but the code around the
association reflections is a bit hairy.

As it looks now I can either use a different join table for each
relationship which would be easy. Or I can use custom finder sql for
the potential_friends relationship.

Any other thoughts would be greatly appreciated.

Thanks again for the help.
Zack


#7

All,
Thanks for everyone how gave ideas on this! It’s working now - for
those who may have a similar situation:

— [ db ] —

create table relationships (
user_id integer unsigned not null,
other_user_id integer unsigned not null,
relation char(1) not null,
foreign key (user_id) references users(id) on update cascade on
delete cascade,
foreign key (other_user_id) references users(id) on update cascade
on delete cascade,
primary key(user_id, other_user_id)
) engine=innodb character set utf8;

create unique index relationships_unique_index on
relationships(user_id, other_user_id);

— [ user.rb snippet ] —

class User < ActiveRecord::Base

has_many :friendships, :class_name => ‘Relationship’, :foreign_key
=> ‘user_id’, :conditions => “relation = ‘f’”
has_many :potential_friendships, :class_name => ‘Relationship’,
:foreign_key => ‘user_id’, :conditions => “relation = ‘r’”
has_many :blocked_relationships, :class_name => ‘Relationship’,
:foreign_key => ‘user_id’, :conditions => “relation = ‘b’”

has_many :friends, :through => :friendships, :source =>
:berelationshipped
has_many :potential_friends, :through => :potential_friendships,
:source => :berelationshipped
has_many :blocked_users, :through => :blocked_relationships, :source
=> :berelationshipped
end

— [ relationship.rb snippet ] —

class Relationship < ActiveRecord::Base
belongs_to :relationshipped, :class_name => ‘User’, :foreign_key =>
‘user_id’
belongs_to :berelationshipped, :class_name => ‘User’, :foreign_key
=> ‘other_user_id’
end

Zack


#8

Hello,

From my research on this it seems that defining has_many :through
associations require that you define an association in your Relationship
class.

in line 163 of reflections.rb

source_reflection_names.collect { |name|
through_reflection.klass.reflect_on_association(name) }.compact.first

what happens here (in your case) would be something like this

[:potential_friend, :potiential_friends].collect { |name|
Relationship.reflect_on_association(name) }.compact.first

since there is no potential_friend, or potential_friends defined in
Relationship, nil is returned hence the Exception.