Reputation: 356
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
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
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