Bi-Directional has_and_belongs_to_many on a Single Model in Rails

560 Views Asked by At

I have User model and I want to create friendship between users. I've achieved this using has_and_belongs_to_many

has_and_belongs_to_many :friends, class_name: 'User', foreign_key: :friend_id

And I have table for this association:

create_table :users_users do |t|
  t.uuid :user_id, null: false
  t.uuid :friend_id, null: false
  t.timestamps
end

When I get friends via user.friends I need to have association where current user is in user_id column or friend_id. My current solution returns me only records where user is in friend_id table.

I have two solution for this:

  1. Duplicate records [{user_id: 1, friend_id: 2}, {user_id: 2, friend_id: 1}]
  2. Override user.friends method and return my custom query

Both solution are not elegant and optimal.

I've read article about it, but the solution from this article is very complicated.

Is there any other solutions for this problem? I'm using rails 5.

2

There are 2 best solutions below

2
On

First of all, I rather use that article solution or one query to fetch all records instead of this.

The active record way to do this is:

  1. create has_many relation instead of HABTM relation
  2. query users_users table to find all records that match user_id=my_user.id or friend_id=my_user.id
  3. flatten the result
  4. unique them
  5. remove current user from array

so I suggest putting this method in User model:

def all_friends
    UserFriend.includes(:user, :friend).where("user_id=? OR friend_id=?", id , id).map {|rel| [rel.user, rel.friend] }.flatten.uniq{|u| u.id}.reject{|u| u.id == self.id}
end

UserFriend model:

class CreateUserFriends < ActiveRecord::Migration[5.0]
  def change
    create_table :user_friends do |t|
      t.belongs_to :user, foreign_key: true
      t.belongs_to :friend, foreign_key: 'friend_id', class: 'User'

      t.timestamps
    end
  end
end
2
On

Not 100% sure that this will work but still wanted to give this to you to try it out before I get some much-needed sleep ;) Specify a custom query method to your association:

has_and_belongs_to_many :friends, -> {
  where("friend_id = :user_id OR user_id = :user_id", user_id: id)
}, class_name: 'User', foreign_key: friend_id