Rails Many to Many with Extra Column - ruby-on-rails

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!

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

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)

Related

Ruby on Rails: has_and_belongs_to_many connection to inherited subclass

I am trying to connect two classes (conversation and user) by a many-to-many relationship in Ruby on Rails. I set them both up and added a connection table called conversations_custom_users to connect them and it was working. Once we needed our User model to inherit from another User model, setting conversations in a user object was failing and looking for a connection table with the parent class.
My classes and the conversation migration looks like below (I haven't modified the User migration for the many-to-many relationship):
class CustomUser < Spree::User
serialize :resources, Array
has_and_belongs_to_many :conversations, :foreign_key => :conversation_ids, class_name: 'Conversation'
end
class Conversation < ApplicationRecord
has_and_belongs_to_many :receiver, :foreign_key => :receiver_id, class_name: 'CustomUser'
end
class CreateConversations < ActiveRecord::Migration[6.1]
def change
create_table :conversations do |t|
t.timestamps
end
create_table :conversations_custom_users, id: false do |t|
t.belongs_to :conversation, foreign_key: 'conversation_id', index: true
t.belongs_to :custom_user, foreign_key: 'receiver_id', index: true
end
end
end
I think I shouldn't need to add another table called conversations_spree_users, but I also tried adding one. It didn't solve the problem since then Rails was looking for a spree_user_id field. I also tried adding the spree_user_id field to the conversations_spree_users table, but it wouldn't migrate because it was a duplicate column name!
I think I'm missing something about many-to-many relations or inheritance or both in Ruby. If someone can help with this issue I'd really appreciate it.
you could use polymorphic associations to build many-to-many association, the benefit of this approach is that you can use only one join-table for all user's hierarchy inheritance.
class CreateConversationals < ActiveRecord::Migration[6.1]
def change
create_table :conversationals do |t|
# ...
t.references :contributor, polymorphic: true, null: false
t.integer :conversation_id
t.timestamps
end
end
end
class Conversational < ApplicationRecord
belongs_to :contributor, polymorphic: true
belongs_to :conversation
end
class Conversation < ApplicationRecord
has_many :conversationals, :foreign_key => :conversation_id
has_many :custom_users, :through => :conversationals, :source => :contributor, :source_type => 'CustomUser'
has_many :other_users, :through => :conversationals, :source => :contributor, :source_type => 'OtherUser'
end
class CustomUser < Spree::User
has_many :conversationals, as: :contributor
has_many :conversations, :through => :conversationals, :as => :contributor
end
# i assume you use STI
class OtherUser < CustomUser
end
then
user1 = CustomUser.create(...)
user2 = OtherUser.create(...)
conversation = Conversation.create(...)
conversational1 = Conversational.create(..., conversation_id: conversation.id, contributor: user1)
conversation1 = Conversational.create(..., conversation_id: conversation.id, contributor: user2)
# many-to-many
user1.conversations
user2.conversations
conversation.custom_users
conversation.other_users

has_many through - Notes sender and receiver

I'm working on a rails app which has three models.
class User < ApplicationRecord; end
class Share < ApplicationRecord; end
class Note < ApplicationRecord; end
create_table :users do |t|
t.timestamps
end
create_table :notes do |t|
t.integer 'user_id'
t.text 'title'
t.text 'short_description'
t.string 'name'
t.timestamps
end
create_table :shares do |t|
t.integer 'user_id'
t.integer 'receiver_id'
t.integer 'note_id'
t.timestamps
end
How can I create associations between them so, I can get
Notes which are shared by User A.
Notes which are received by User A.
Notes which are created by User A.
#Mehmet Adil İstikbal gives part of the answer so I'll try to complete it.
This is another way to do it using only associations :
class User < ApplicationRecord
has_many :created_notes, class_name: 'Note', foreign_key: :user_id
has_many :received_shares, foreign_key: :receiver_id, class_name: 'Share'
has_many :received_notes, through: :received_shares, source: :note
has_many :shares
has_many :shared_notes, through: :shares, source: :note
end
class Share < ApplicationRecord
# Optional
belongs_to :creator, class_name: 'User', foreign_key: :user_id
belongs_to :receiver, class_name: 'User', foreign_key: :receiver_id
# Mandatory
belongs_to :note
end
class Note < ApplicationRecord ; end
user_a = User.first
user_a.shared_notes
user_a.received_notes
user_a.created_notes
If you choose #Mehmet Adil İstikbal answer, please make sure to transform
user.shares.each {|share| share.note} to user.shares.map(&:note) (Use map and not each)
My answer uses has_many through association which allows you to go "through" join table.
In user model you can do like this:
has_many :shares, foreign_key: 'user_id', class_name: 'Share', dependent: :destroy
has_many :receives, foreign_key: 'receiver_id', class_name: 'Share', dependent: :destroy
and you can call like this:
User.first.shares.each {|share| share.note}
This will get all shares with first users id and all of their notes.
For receiver :
User.first.receives.each {|share| share.note}
In your share model you can also specify the opposite connection like this:
belongs_to :sender, foreign_key: 'user_id', class_name: 'User'
belongs_to :receiver, foreign_key: 'receiver_id', class_name: 'User'
With this you can call:
Share.first.receiver this will get you to user that receives this post
And for the notes which are created by user you can call:
User.first.notes
You may want to delete those dependents in order to your project.
Hope it helps

How to set up an admin user in rails

I have a simple relationship
class School < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :schools
end
A user can be part of many schools but at the same time a user might be the admin of a number of schools. I set up a many-to-many relationship to represent this however I'm not sure how I would distinguish between admins and simple users.
I initially thought of setting a table which has a school_id and a user_id and every entry will represent the school id and the user id of any admins that the school has however I'm not sure how I would represent this in rails or if it's the best way to solve this problem? And if it is, how do I access the table without a model associated to it?
What I mean by what I said above:
school_id user_id
1 3
1 4
Which means that the school with id 1 has 2 admins (3 and 4)
What you are looking for is a more complex many_to_many relationship between school and user called has_many :through. This relationship allows you to have many to many relationship with access to the table that represents the relationship. If you use that relationship, your models should look something like this:
class User < ActiveRecord::Base
has_many :school_roles
has_many :schools, through: :school_roles
end
class SchoolRole < ActiveRecord::Base
belongs_to :school
belongs_to :user
end
class School < ActiveRecord::Base
has_many :school_roles
has_many :users, through: :school_roles
end
And the migrations of those tables would look something like this:
class CreateSchoolRoles < ActiveRecord::Migration
def change
create_table :schools do |t|
t.string :name
t.timestamps null: false
end
create_table :users do |t|
t.string :name
t.timestamps null: false
end
create_table :school_roles do |t|
t.belongs_to :school, index: true
t.belongs_to :user, index: true
t.string :role
t.timestamps null: false
end
end
end
I would suggest to make the "role" field in the "school_roles" migration an integer and then use an enum in the model like so:
class SchoolRole < ActiveRecord::Base
belongs_to :school
belongs_to :user
enum role: [ :admin, :user ]
end
which allows you to add more roles in the future, but it's your call
combining polymorphic association with has_many :through in my opinion is best option.
Let's say you create supporting model SchoolRole, which
belongs_to :user
belongs_to :school
belongs_to :rolable, polymorphic:true
This way:
class School ...
has_many :administrators, :as => :schoolroles
has_many :users, :through => :administators
#school.administrators= [..., ...]
It is quite agile.
#user=#school.administrators.build()
class User
has_many :roles, :as => :rolable
def admin?
admin=false
self.roles.each do |r|
if r.role_type == "administator"
admin=true
break
end
end
admin
end
....

Rails has_and_belongs_to_many relations with two types of Users and one type of Table

I have a problem related with this association. A pasted code is better than any title:
table.rb
class Table < ActiveRecord::Base
has_and_belongs_to_many :clients, class_name: 'User'
has_and_belongs_to_many :managers, class_name: 'User'
end
user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :tables
end
migration - join table
class UsersToTable < ActiveRecord::Migration
def change
create_table :tables_users, id: false do |t|
t.references :user, as: :client
t.references :user, as: :manager
t.references :table
end
end
end
Problem
tab = Table.new
tab.save
tab.clients.create
tab.clients.create
tab.clients.create
tab.managers.create
tab.managers.size # == 4
tab.clients.size # == 4
When I creating associated Objects(Users) they all are linked to both clients and managers.
I want to be able to create them separately - When creating a client - only number of clients rise, when creating manager, only number of managers rise.
In other words I want this:
tab.managers.size # == 1
tab.clients.size # == 3
Could you please help?
has_and_belongs_to_many :stuff, class_name: 'StuffClass' is just DSL for:
has_many "<inferred_join_table_name>"
has_many :stuff, through: "<inferred_join_table_name>"
It seems that since clients and managers are names for Users, the inferred join table get's to be "TablesUsers", and that is not right.
Try specifyng the join table for both and using different join tables for each relationship:
class Table
has_many :tables_clients
has_many :clients, through: :tables_clients
has_many :tables_managers
has_many :clients, through: :tables_managers
end
class TablesClients
belongs_to :client, class_name: 'User'
belongs_to :table
end
create_table :tables_clients, id: false do |t|
t.references :client, index: true
t.references :table, index: true
end
# and the same for tables_managers
Then the user belongs to Tables in too different ways:
class User
has_many :client_tables_users, class_name: 'TablesUsers', foreign_key: :client_id
has_many :tables_as_client, through: :client_tables_users, source: :table
has_many :managed_tables_users, class_name: 'TablesUsers', foreign_key: :manager_id
has_many :managed_tables, through: :managed_tables_users, source: :table
end

Setting up a polymorphic association

I am trying to add a "following" like functionality to my site but I am having trouble finding the right way to use a polymorphic association. A user needs to be able to follow 3 different classes, these 3 classes do not follow the user back. I have created a user following user in the past but this is proving to be more difficult.
My Migration was
class CreateRelationships < ActiveRecord::Migration
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :relations_id
t.string :relations_type
t.timestamps
end
end
end
My Relationship model is
class Relationship < ActiveRecord::Base
attr_accessible :relations_id
belongs_to :relations, :polymorphic => true
has_many :followers, :class_name => "User"
end
In my User model
has_many :relationships, :foreign_key => "supporter_id", :dependent => :destroy
and in the other 3 models
has_many :relationships, :as => :relations
Am I missing something with setting up this association?
You basically have it right, except for a few minor errors:
attr_accessible :relations_id is redundant. Remove it from your Relationship model.
Both Relationship and User models call has_many to associate with each other. Relationship should call belongs_to because it contains the foreign key.
In your User model, set :foreign_key => "follower_id".
Here is how I would do it.
Have a Follow middle class with polymorphic association on the followable content side and has_many on the follower user side (user has many follows).
First, create a follows table:
class CreateFollows < ActiveRecord::Migration
def change
create_table :follows do |t|
t.integer :follower_id
t.references :followable, :polymorphic => true
t.timestamps
end
end
end
Replace Relationship model with a Follow model:
class Follow < ActiveRecord::Base
belongs_to :followable, :polymorphic => true
belongs_to :followers, :class_name => "User"
end
Include in User model:
has_many :follows, :foreign_key => :follower_id
Include in your three followable classes:
has_many :follows, :as => :followable
You can now do this:
TheContent.follows # => [Follow,...] # Useful for counting "N followers"
User.follows # => [Follow,...]
Follow.follower # => User
Follow.followable # => TheContent

Resources