Saturday, July 28, 2007

Self-referential many-to-many join with extra data and non-standard foreign keys

Here's another gotcha that I ran into that's kind of an edge case in Rails. So I'm documenting it, so that the rest of you can go about your business of building great apps, instead of scratching your heads. So what was I doing? I wanted a self-referential join table with rich associations for an "acts_as_friendable" plugin I was writing.

I made table in database as follows:
  create_table "friendships", :force => true do |t|
t.column "owner_id", :integer, :null => false
t.column "friend_id", :integer, :null => false
end

In the model, put:
  class Friendship < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => :owner_id
belongs_to :friend, :class_name => "User", :foreign_key => :friend_id
end

Then in the table that you want to act as friendable, you put:
  class User < ActiveRecord::Base

acts_as_friendable

end

And that's it. you can now access friends() and friendships() of a person.

However, it doesn't work when you try to push the association through, like:
user = User.find(1)
friend = User.find(2)
user.friends << friend

It will fail complain about how "user_id" doesn't exist in any of the tables. I was scratching my head for a good 2 hours before I figured that it wasn't actually me this time. I think the << method doesn't correctly use the foreign keys correctly when they're non-standard like this. According to #6466 ([PATCH] fix for has_many :through push and delete with legacy/non-standard foreign_keys), this doesn't work, until a patch is written for it. I'm not as familiar with the Rails code base as I should be. This is probably a good chance to get started looking at it...unless someone else fixes it first...

As a workaround, I simply used the Friendship ActiveRecord object directly to create the association. I'll have to override the method in the plugin so that it allows correct use of << for my specific case. Tip!

2 comments:

  1. Nice work! How do you get your plugin? Alex

    ReplyDelete
  2. It was a custom plugin that I wrote. I never published it because I didn't think anyone else would want to use it.

    ReplyDelete