Self referential association in Rails - ruby-on-rails

I am trying to figure out how to do a self referencing association in Rails. I'm a Rails beginner.
Basically, I have a model Group. Each Group can have many sub-groups. I feel like I've tried everything, but I can't get the join to work.
What I have now is
# GroupSubGroup Model
class GroupSubGroup < ApplicationRecord
belongs_to :group
belongs_to :sub_group, class_name: 'Group'
end
and then my Group Model looks like
has_many :group_sub_groups
has_many :sub_groups, foreign_key: :sub_group_id, through: :group_sub_groups, class_name: 'GroupSubGroup'
has_many :groups, through: :sub_groups
has_many :groups, class_name: 'GroupSubGroup'
And my migration looks like
create_table :group_sub_groups do |t|
t.integer :group_id, index: true, foreign_key: { to_table: :groups }
t.references :sub_group, index: true, foreign_key: { to_table: :groups }
t.timestamps
end
My main issue is that I can add a new GroupSubGroup row into the join table using parent_group.sub_groups.new, however when I retrieve the parent group and loop over it's sub_groups, none of the instances are of the Group class and therefore don't have any of the methods.
For example
Group.all.each do |group|
group.sub_groups.each do |s|
puts "#{s.name} is a sub group for #{group.name}"
end
end
Throws an undefined method 'name' error.

Its actually a lot simpler then this. You don't need a separate model/table for subgroups. Which is the whole of the point of a self referential association.
Lets just start out with the groups table and add our self-refential foreign key:
class CreateGroups < ActiveRecord::Migration[6.0]
def change
create_table :groups do |t|
t.references :parent, index: true, foreign_key: { to_table: :groups }
t.string :name
t.timestamps
end
end
end
Then lets create a one-to-many association to the same table:
class Group < ApplicationRecord
belongs_to :parent,
class_name: 'Group',
inverse_of: :sub_groups
has_many :sub_groups,
class_name: 'Group',
foreign_key: 'parent_id',
inverse_of: :parent
scope :top_level, ->{ where(parent_id: nil) }
end
You can then iterate through the top-level groups and their subgroups with:
# eager_load prevents an n+1 query
Group.top_level.eager_load(:sub_groups).each do |group|
group.sub_groups.each do |s|
puts "#{s.name} is a sub group for #{group.name}"
end
end

I think it would be how you set up the associations
has_many :group_sub_groups
# The following should suffice for sub groups. This should return all
# sub_groups that the group has
has_many :sub_groups, through: :group_sub_groups, source: :sub_group
# The following should suffice for groups. This should return all groups
# where the group is a sub_group of.
has_many :groups, through: :group_sub_groups, source: :group
For the following code
Group.all.each do |group|
group.sub_groups.each do |s|
puts "#{s.name} is a sub group for #{group.name}"
end
end
The error is most probably because of s.name. In your original implementation, sub_groups has a class of class_name: 'GroupSubGroup' which is not what you want. Using the associations I mentioned above should fix that error.

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

How to create a self-referencing model

I've found a bunch of previously answered questions, or articles, about self-referencing has_many relationships, but not that address what I'm trying to do. I have a model Group and a join model that's meant to keep track of information about the relationships between groups. Here's what they look like so far:
class Group < ApplicationRecord
has_many :relationships
has_many :relations, through: :relationships
end
class Relationships < ApplicationRecord
belongs_to :group
belongs_to :relation, class_name: 'Group'
end
The answers I've found until now present something like that, and that works alright but isn't what I want. Because if I create a Relationship from Group A to Group B, A.relations contains B (because B is the 'relation' in the Relationship) but B.relations does not contain A (because A is the 'group' in the Relationship).
The relationships I'm modeling aren't hierarchical, which means I'd like them to go both ways. I'd hoped something like this might be possible, but I can't find any information:
class Relationships < ApplicationRecord
belongs_to :group_one, class_name: 'Group'
belongs_to :group_two, class_name: 'Group'
end
class Group < ApplicationRecord
has_many :relationships, :through => (:group_one || :group_two)
end
I realize that won't work, but I'm trying to illustrate what I want to do. Ideally I'd like it if a.relationships came back with an array of every Relationship where Group A is in either the group_one_id or group_two_id column, and where a.relations likewise followed the same pattern. I'd use a custom validation to ensure that :group_one and :group_two never referred to the same Group.
Is this possible?
You can do this for Group model:
class Group < ApplicationRecord
has_many :relationships, foreign_key: :group_one_id
has_many :relations, through: :relationships, source: :group_two
end
And this for Relationship model:
class Relationship < ApplicationRecord
belongs_to :group_one, class_name: 'Group', foreign_key: :group_one_id
belongs_to :group_two, class_name: 'Group', foreign_key: :group_two_id
end
And your migration should look like this:
Example for Groups table:
class CreateGroups < ActiveRecord::Migration[6.0]
def change
create_table :groups do |t|
t.string :name
t.timestamps
end
end
end
Example for Relationships table:
class CreateRelationships < ActiveRecord::Migration[6.0]
def change
create_table :relationships do |t|
t.references :group_one, null: false, references: :groups, foreign_key: { to_table: :groups }
t.references :group_two, null: false, references: :groups, foreign_key: { to_table: :groups }
t.timestamps
end
end
end
With this in place, you can create groups and associated each other with specific group.
group1 = Group.create(name: 'Group 1')
group2 = Group.create(name: 'Group 2')
group3 = Group.create(name: 'Group 3')
group1.relations << group2
group1.relations << group3
Not running group1.relations should return both group2 and group3

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!
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)

Two or more Many to Many relationships between two tables in rails

I have two tables:
Users and Groups
a User has_many groups and a group, has_many a users:
u = User.last
u.groups
g = Group.last
g.users
Supposed I wanted a second list of different groups, for some strange reason. Where once again a User has may groups (called other_group in this example) and a group has many users.
u = User.last
u.other_groups
g = Group.last
g.other_users
How do I associate two models in this relationship, twice using Active Record? Do I need multiple has and belongs to many tables? perhaps a has and belongs to many "through". What does this look like?
Answer:
class Matter < ActiveRecord::Base
has_many :matters_lawfirms
has_many :matters_other_lawfirms
has_many :lawfirms, class_name: 'Lawfirm', through: :matters_lawfirms, :source => :lawfirm
has_many :other_lawfirms, class_name: 'Lawfirm', through: :matters_other_lawfirms, :source => :lawfirm
end
class Lawfirm < ActiveRecord::Base
has_many :matters_lawfirms
has_many :matters_other_lawfirms
has_many :matters, class_name: 'Matter', through: :matters_lawfirms, :source => :matter
has_many :other_matters, class_name: 'Matter', through: :matters_other_lawfirms, :source => :matter
end
class MattersLawfirm < ActiveRecord::Base
belongs_to :matter
belongs_to :lawfirm
end
class MattersOtherLawfirm < ActiveRecord::Base
belongs_to :matter
belongs_to :lawfirm
end
migrations:
class AddMatterOtherLawfirms < ActiveRecord::Migration
def change
create_table :matters_other_lawfirms, :id => false do |t|
t.references :matter, :lawfirm
end
add_index :matters_other_lawfirms, [:matter_id, :lawfirm_id],
name: "matters_other_lawfirms_index",
unique: true
end
end
class AddMatterLawfirmsHabtmt < ActiveRecord::Migration
def change
create_table :matters_lawfirms, :id => false do |t|
t.references :matter, :lawfirm
end
add_index :matters_lawfirms, [:matter_id, :lawfirm_id],
name: "matters_lawfirms_index",
unique: true
end
end
Assuming you already know how to handle has_many relationships in your data model (the point to which Alper appears to be writing), it's quite easy to accomodate multiple relationships between two tables (I do something right now which involves linking users distinctly to a projects on which they are working, and to projects which they own. I believe this is very similar to what you're seeking to accomplish). The code would look something like this:
User Model
has_many :regular_groups, class_name: 'Group', through: :user_regular_groups
has_many :other_groups, class_name: 'Group', through: :user_other_groups
Group Model
has_many :regular_users, class_name: 'User', through: :user_regular_groups
has_many :other_users, class_name: 'User', through: :user_other_groups
Obviously in this case we're using two distinct association tables (user_regular_groups and user_other_groups), but you could accomplish something similar with one using a scope (along the lines of what Alper is recommending).
I hope that helps!
You simply can't define many to many relationship using belong_to
You should implement has_and_belongs_to_many or has_many :through relationship instead of has_many - belongs_to relationship.
EDIT
Ok i think i get it now,
You can't achive this using a single has_and_belongs_to_many table, i'd go for a has_many :through relation. If you have only two group category, set a flag for that category in your join table.
Not tested yet, but something similar should work
class GroupMembership #.. with category field or something
belongs_to :group
belongs_to :user
class User
has_many :group_memberships
has_many :local_groups, -> { where group_memberships: { category: 'local' } }, :through => :group_memberships, :source => :group
has_many :outside_groups, -> { where group_memberships: { category: 'outside' } }, :through => :group_memberships, :source => :group
class Group
has_many :group_memberships
has_many :local_users, -> { where group_memberships: { category: 'local' } }, :through => :group_memberships, :source => :user
has_many :outside_users, -> { where group_memberships: { category: 'outside' } }, :through => :group_memberships, :source => :user
For the HABTM relationship, you need to define more then one join table.

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

Resources