Rails Many to Many with Extra Column

So I have these tables:

create_table :users do |t|
  t.string :username
  t.string :email
  t.string :password_digest

  t.timestamps
end

create_table :rooms do |t|
  t.string :name
  t.string :password
  t.integer :size
  t.integer :current_size

  t.timestamps
end

create_table :rooms_users do |t|
  t.belongs_to :user, index: true
  t.belongs_to :room, index: true

  t.boolean :is_admin

  t.timestamps
end

I made it so, when I call Room.find(1).users I get a list of all the users in the room. However, I also want to be able to call something like Room.find(1).admins and get a list of users that are admins (where is_admin in rooms_users is true). How would I do that?

Thank you for your time!

Upvotes: 3

Views: 1522

Answers (2)

MrYoshiji
MrYoshiji

Reputation: 54882

You can define a proc in the has_many relation to set SQL clauses, like ORDER or WHERE:

# room.rb
has_many :rooms_users, class_name: 'RoomsUser'
has_many :users, through: :rooms_users
has_many :admins, 
  proc { where(rooms_users: { is_admin: true }) },
  through: :rooms_users,
  class_name: 'User',
  source: :users

# user.rb
has_many :administrated_rooms,
  proc { where(rooms_users: { is_admin: true }) },
  through: :rooms_users,
  class_name: 'Room',
  source: :rooms

You can simplify this with a simple scope defined in the RoomsUser model, something like:

# rooms_user.rb
scope :as_admins, -> { where(is_admin: true) }

And use it in the proc:

# user.rb
has_many :administrated_rooms,
  proc { as_admins },
  through: :rooms_users,
  class_name: 'Room',
  source: :rooms

source option explained:

With source: :users, we're telling Rails to use an association called :users on the RoomsUser model (as that's the model used for :rooms_users).

(from Understanding :source option of has_one/has_many through of Rails)

Upvotes: 0

max
max

Reputation: 101811

You want to use has_many through: instead of has_and_belongs_to_many. Both define many to many associations but has_many through: uses a model for the join rows.

The lack of a model makes has_and_belongs_to_many very limited. You cannot query the join table directly or add additional columns since the rows are created indirectly.

class User < ApplicationRecord
  has_many :user_rooms
  has_many :rooms, through: :user_rooms
end

class Room < ApplicationRecord
  has_many :user_rooms
  has_many :users, through: :user_rooms
end

class UserRoom < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

You can use your existing schema but you need to rename the table users_rooms to user_rooms with a migration - otherwise rails will deride the class name as Rooms::User.

class RenameUsersRooms < ActiveRecord::Migration[5.0]
  def change
    rename_table(:users_rooms, :user_rooms)
  end
end

However, I also want to be able to call something like Room.find(1).admins and get a list of users that are admins (where is_admin in rooms_users is true). How would I do that?

You want to use a left inner join:

User.joins(:user_rooms)
    .where(user_rooms: { room_id: 1, is_admin: true })

To roll that into the class you can setup an association with a scope applied:

class Room < ApplicationRecord
  has_many :user_rooms
  has_many :users, through: :user_rooms
  has_many :user_room_admins, class_name: 'UserRoom', ->{ where(is_admin: true) } 
  has_many :user_room_admins, through: :user_rooms, 
    class_name: 'User',
    source: :user
end

Upvotes: 4

Related Questions