Need a little help perfecting my friendship associations

Hey,

I’ve somehow managed to get friendships working first try, but it’s not perfect.

First the code:

create_table :friendships do |t|
t.column :initiator_id, :integer, :null => false

t.column    :recipricator_id,   :integer,   :null => false
t.column    :confirmed,         :boolean,   :null => false,     :default => 0

end

class Friendship < ActiveRecord::Base
belongs_to :recipricator,

           :foreign_key => 'recipricator_id',
           :class_name => 'User'
belongs_to :initiator,
           :foreign_key => 'initiator_id',
           :class_name => 'User'

end

class User < ActiveRecord::Base
has_many :initiated_friendships,
:foreign_key => ‘initiator_id’,
:class_name => ‘Friendship’,
:conditions => ‘confirmed = 1’

has_many :recipricated_friendships,
         :foreign_key => 'recipricator_id',
         :class_name => 'Friendship',
         :conditions => 'confirmed = 1'
has_many :unconfirmed_friendship_requests,

         :foreign_key => 'recipricator_id',
         :class_name => 'Friendship'
has_many :unconfirmed_friendship_proposals,
         :foreign_key => 'initiator_id',
         :class_name => 'Friendship'

end

My first problem is that to get a User object from my friendship I’ve to do something like:
current_user.unconfirmed_friendship_requests.first.initiator # Gets the User object of the user that made the friendship request

I’d like ‘unconfirmed_friendship_requests’ to contain all users that were associated with the initiator_id, I don’t care about the recipricator_id as that’s always the current user. The same goes for ‘unconfirmed_friendship_proposals’ but the opposite.

The second problem, is that to get all the friends of a user I need to iterate over initiated_friendships and recipricated_friendships. I’d like to just do current_user.friends.

btw, I don’t like those friendship systems where you can befriend a user despite them not wanting you to, hence why I’ve used a confirmed flag.

Cheers,
Ian.

Ian,

Looks like a social networking site in the makings.

To my eyes you're on the right track, but if you stop here you're going
to end up with a lot of extra code. If you go ahead and make your
model a bit more verbose I think you can really shorten up the indexes
into the different types of relationships.

This piece you're model is missing is the "has_many :through" concept.
Your model is an interesting unary many to many relationship. (Many
users can have many unconfirmed friends, etc...). You're model is spot
on with friendship table facilitating the different joins.

class User < ActiveRecord::Base
    has_many :initiated_friendships,
             :foreign_key => 'initiator_id',
             :class_name => 'Friendship',
             :conditions => 'confirmed = 1'
    has_many :reciprocated_friendships,
             :foreign_key => 'reciprocator_id',
             :class_name => 'Friendship',
             :conditions => 'confirmed = 1'
    has_many :unconfirmed_friendship_requests,
             :foreign_key => 'recipricator_id',
             :class_name => 'Friendship',
             :conditions => 'confirmed = 0'
    has_many :unconfirmed_friendship_proposals,
             :foreign_key => 'initiator_id',
             :class_name => 'Friendship',
             :conditions => 'confirmed = 0'

    has_many :initiated,
             :through => 'initiated_friendships'
             :foreign_key => 'reciprocator_id'
    has_many :reciprocated,
             :through => 'reciprocated_friendships'
             :foreign_key => 'initiator_id'
   has_many :unconfirmed_requests,
             :through => unconfirmed_friendship_requests,
             :foreign_key => 'initiator_id'
   has_many :unconfirmed_proposals,
             :through => unconfirmed_friendship_proposals,
             :foreign_key => 'recipricator_id'
end

The real evangelist of this style would be Josh Susser of
http://www.hasmanythrough.com . His site has a lot of great examples
if you don't mind digging through his blog for them.

In the above code I've never had to use a foreign key in a has_man
:through association so I'm not sure it works the way I've demonstrated
(I don't have the time right now to put together a test so it's just a
theory.

If this does indeed function this would take the command to index form:
current_user.unconfirmed_friendship_requests.first.initiator
to:
current_user. unconfirmed_requests.first

Another thought I had while making the modifications was that it might
be simpler in the long run to us a directional association that
indicates whether a person is considered a friend or not. When the
relationship is only made in a single direction it is unconfirmed with
the connector being the initiator and the connected to being the
reciprocator. Once the reciprocator confirms by making a connection
themselves we don't really care which of them initiated, or at-least I
would imagine that the initiator/recipricator roles would be less
important after the relationship is fully formed.

The big advantage would eliminating an sql request every time a friends
list is drawn up. In order to compile a list of all friends right now
you will need to grab both initiated and reciprocated confirmed
friendships (2 sql requests). If I understand your application
correctly this would be far superior to be able to request all
confirmed friends in a single request. If this is indeed a social
networking site the listing of friends will probably be one of the most
frequent database actions so if we can cut the execution time for this
in half in the long run we'll generate a lot of efficiency.

Anyway. Thats my $.02 for what it's worth.

-Aaron

Thanks for the excellent reply Aaron.

The reason I had avoided using a record for each direction of friendship was to avoid what in my eyes is duplicate record.
I was aware of the two query overhead, though I was hoping rails would be able to do some OR magic on the JOIN for me. I may just go with the the two friendships record for a confirmation approach, it seems a lot simpler. This is as far as my current knowledge of Rails associations will stretch. Rails is going to attempt a join on either the initiator or the recipricator, not both of them ANDed, correct? Am I going to have to use :finder_sql?

On second thoughts, could I just not user :finder_sql with the single record approach and do the JOIN magic myself? That’d reduce it to one query and one record.

I think i’ll give this a try in the morning…

Ian,

After all that I knew I'd seen something like this before. Josh Susser
(mentioned previously) wrote a post about self referencial maps several
months back. I went back and found it. Low and behold one of the
commentors is building a very similar system of references. Anyway
here's the link:
http://blog.hasmanythrough.com/articles/2006/04/21/self-referential-through

I think you'll have to actually write the sql yourself to get OR syntax
merging the two sets. I'm not aware of any rails sugar available to do
so, which doesn't mean it doesn't exist, it just means I haven't heard
of it yet if it does.

Glad to be of assistance,

Aaron - stocad@gmail.com