How to create a self-referencing model - ruby-on-rails

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

Related

Self referential association in 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.

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
....

many to many polymorphic association

I'm not sure how to create this, I'd like to create a many-to-many polymorphic association.
I have a question model, which belongs to a company.
Now the question can has_many users, groups, or company. Depending on how you assign it.
I'd like to be able to assign the question to one / several users, or one / several groups, or the company it belongs to.
How do I go about setting this up?
In this case I would add a Assignment model which acts as an intersection between questions and the entities which are assigned to it.
Create the table
Lets run a generator to create the needed files:
rails g model assignment question:belongs_to assignee_id:integer assignee_type:string
Then let's open up the created migration file (db/migrations/...__create_assignments.rb):
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.integer :assignee_id
t.string :assignee_type
t.belongs_to :question, index: true, foreign_key: true
t.index [:assignee_id, :assignee_type]
t.timestamps null: false
end
end
end
If you're paying attention here you can see that we add a foreign key for question_id but not assignee_id. That's because the database does not know which table assignee_id points to and cannot enforce referential integrity*. We also add a compound index for [:assignee_id, :assignee_type] as they always will be queried together.
Setting up the relationship
class Assignment < ActiveRecord::Base
belongs_to :question
belongs_to :assignee, polymorphic: true
end
The polymorpic: true option tells ActiveRecord to look at the assignee_type column to decide which table to load assignee from.
class User < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Company < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
Unfortunately one of the caveats of polymorphic relationships is that you cannot eager load the polymorphic assignee relationship. Or declare a has_many :assignees, though: :assignments.
One workaround is:
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
def assignees
assignments.map(&:assignee)
end
end
But this can result in very inefficient SQL queries since each assignee will be loaded in a query!
Instead you can do something like this:
class Question < ActiveRecord::Base
has_many :assignments
# creates a relationship for each assignee type
['Company', 'Group', 'User'].each do |type|
has_many "#{type.downcase}_assignees".to_sym,
through: :assignments,
source: :assignee,
source_type: type
end
def assignees
(company_assignees + group_assignees + user_assignees)
end
end
Which will only cause one query per assignee type which is a big improvement.

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

Using has_many through in Rails 4

I'm actually trying to create a has_many through association. Let me first explain a bit about how things are supposed to work.
I have a users, groups and members tables. The rules are as follow :
A user can create a group (depending on it's role) (groups table has a user_id)
A user can be member of one or many groups (members table contain user_id and group_id)
Here is my current relationship classes :
class User < ActiveRecord::Base
# Associations
has_many :groups # As user, I create many groups
has_many :members
has_many :groups, through: :members # But I can also belongs to many groups
end
class Group < ActiveRecord::Base
# Associations
belongs_to :user
has_many :members
has_many :users, through: :members
end
class Member < ActiveRecord::Base
# Associations
belongs_to :user
belongs_to :group
end
My problem is about the group relationship. You see a user can create groups, which means :
has_many :groups
but a user can also be member of groups :
has_many :groups, through: :members
Because of this new relationship, 75% of my specs are now broken. Also, I notice that if I logged in with a user associated to a group, I can see actually groups list. But when I'm trying to logged in as group owner (the one who created the group), I can not see the groups created by that user).
Idea?
You are not looking for an has_many through relationship here
Try that :
class User < ActiveRecord::Base
# Associations
has_and_belongs_to_many :groups
has_many :created_groups, class_name: 'Group', foreign_key: 'creator_id'
end
class Group < ActiveRecord::Base
# Associations
belongs_to :creator, class_name: 'User'
has_and_belongs_to_many :members, class_name: 'User'
end
This is a solution if you don't need the member class to do any special treatment.
You should have a migration that looks like that:
class CreateGroupsUsers < ActiveRecord::Migration
def change
create_table :groups_users, id: false do |t|
t.references :group
t.references :user
end
add_index :groups_users, [:group_id, :user_id]
add_index :groups_users, :user_id
end
end
And you have to make sure that your groups table have a creator_id !

Resources