1
votes

I have a User model, that I want to connect to itself through a self join using has_and_belongs_to_many. I have it working almost as expected, except I want it to associate the two users both ways.

My User class:

class User < ActiveRecord::Base
  ...
  has_and_belongs_to_many :friends,
      autosave: true,
      class_name: 'User',
      join_table: :friendships,
      foreign_key: :user_id,
      association_foreign_key: :friend_user_id
  ...
end

My migration:

class CreateFriendships < ActiveRecord::Migration
  def self.up
    create_table :friendships, id: false do |t|
      t.integer :user_id
      t.integer :friend_user_id
    end

    add_index(:friendships, [:user_id, :friend_user_id], :unique => true)
    add_index(:friendships, [:friend_user_id, :user_id], :unique => true)
  end

  def self.down
    remove_index(:friendships, [:friend_user_id, :user_id])
    remove_index(:friendships, [:user_id, :friend_user_id])
    drop_table :friendships
  end
end

My problem:

user1 = User.find(1)
user2 = User.find(2)

user1.friends << user2
user1.reload.friends.exists?(user2) # true
user2.reload.friends.exists?(user1) # false <- My problem

How can I make the relationship work both ways? As friendships are always mutual in this context and other questions on SO make it seem as this should be possible, I would like for both the last two statements to return true.

2
You can also check out the solution I posted for a similar question on SO.Joe Kennedy

2 Answers

2
votes

The less hackish approach is to build friendships inside of your own method:

class User < ActiveRecord::Base
  def make_friend(user)
    # TODO: put in check that association does not exist
    self.friends << user
    user.friends << self
  end
end

And to call it like

user1.make_friend(user2)
# should set both friends know about each other

The more hackish one is to trick with ActiveRecord::Associations methods overriding. E.g. has_and_belongs_to_many's method collection<<(object, …) can be revised for your case with something like that:

class User < ActiveRecord::Base
  attr_accessor :reversed  # we use it to avoid stack level too deep issue
  has_and_belongs_to_many :friends, ... do
    def << (new_friend)
      reversed = true
      # it should not trigger on our friend record as self.reversed is true
      new_friend.friends << self unless new_friend.reversed
      super new_friend
    end     
  end
end

NOTE: I'm not sure about self meaning inside of << method, so may be you should dig the real object instance somehow through relation object.

0
votes

You can also do...

user2.friends << user1

It does mean there are two join records.