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