I have this app: Task Manager.
I have a User.
A user can create one or more Groups -> then it becomes admin of the groups it created
At the same time a user can be just a Member (a middle table between User and Group) of a Group created by another one.
So I have that User:
has_many :groups, foreign_key: :admin_id, dependent: :destroy
has_many :groups, through: :members
And now I want to ask to the Db:
Give me the groups where user is admin
Give me the groups where user is just a user
I can most likely create a SQL query for that but I thought there would be a more Rails way of doing things.
Is there a way to do this? I can only think of having User but at the same time dividing it into 2 different subclasses like UserAdmin and NormalUser. But I am not sure on how to do that or if it is even the right approach.
Thank you in advance!
Splitting your User class into multiple classes isn't the answer as you want users to be able to have different roles in different groups. Rather there are few different solutions.
1. Add a foreign key column
If you want to have a specific one to many assocation between a user and a group you can add a separate assocation:
class AddCreatorToGroups < ActiveRecord::Migration[7.0]
def change
# note that you'll have to worry about filling this
# column if you have existing data
add_reference :groups, :founder, null: false, foreign_key: { to_table: :users }
end
end
class Group
# ...
belongs_to :founder,
class_name: 'User',
inverse_of: :groups_as_founder
end
class User
# ...
has_many :groups_as_founder,
class_name: 'Group',
foreign_key: :founder_id,
inverse_of: :founder
end
This is a good idea if you need to be able to eager load the group and just that user very efficiently. This would be set when creating the group:
def create
#group = Group.new(group_params) do |group|
group.founder = current_user
end
# ...
end
This can be combined with the other options. It won't solve the case where you want to assign multiple admins to a group.
2. Add the roles to the memberships
class AddCreatorToGroups < ActiveRecord::Migration[7.0]
def change
add_columns :memberships, :role, :integer, default: 0
end
end
class Membership < ApplicationRecord
belongs_to :group
belongs_to :member, class_name: 'User'
enum :role, {
normal: 0,
admin: 1
}
end
class Group < ApplicationRecord
has_many :normal_memberships,
class_name: 'Membership',
-> { Membership.normal }
has_many :admin_memberships,
class_name: 'Membership',
-> { Membership.admin }
has_many :normal_members,
class_name: 'User',
through: :normal_memberships
has_many :admin_members,
class_name: 'User',
through: :admin_memberships
end
class User < ApplicationRecord
has_many :normal_memberships,
class_name: 'Membership',
-> { Membership.normal }
has_many :admin_memberships,
class_name: 'Membership',
-> { Membership.admin }
has_many :groups_as_normal_member,
class_name: 'Group',
through: :normal_memberships
has_many :groups_as_admin_member,
class_name: 'Group',
through: :admin_memberships
end
This is a greatly simpliefied example where the available roles are defined through an enum on the memberships table. A more complex example could have the role defined as role_id pointing to a separate table.
This repition can be avoided somewhat by looping across Membership.roles.key.
One thing to consider is that it places a lot of responsibities into the Membership class.
3. A separate role system.
An additional alternative would be to use a separate system such as Rolify and a completely different set of tables to store the roles in a group. There are plenty of tutorials on how to use Rolify or build a role based access system from scratch if you search a bit.
Related
I want to create manager->employee association.
Manager have many employees
Employee belongs to only one manager
Manager does not have another manager
How to implement this design ?
I created something closer(code is below) but in my design manager can have other manager.
class User < ApplicationRecord
has_many :employee, class_name: "User", foreign_key: "manager_id"
belongs_to :manager, class_name: "User", foreign_key: "manager_id"
end
Any help is highly appreciated!
What you have is a really standard self-joining setup (with a few small issues). You can limit the depth of the tree by just adding a custom validation:
class User < ApplicationRecord
# optional: true is needed in Rails 5+ since belongs_to is no
# longer optional by default
belongs_to :manager, class_name: 'User',
optional: true
# pay attention to pluralization
has_many :employees, class_name: 'User',
foreign_key: 'manager_id'
# calls the custom validation we will define later
validate :manager_must_be_a_manager, unless: :manager?
def manager?
manager.nil?
end
private
# our custom validation method
def manager_must_be_a_manager
errors.add(:manager, 'is not a manager') unless manager.manager?
end
end
So now if we run:
user_1 = User.create!
user_2 = User.create!(manager: user_1)
User.create!(manager: user_2)
The third line will raise ActiveRecord::RecordInvalid: Validation failed: Manager is not a manager.
I think it's better to have one more field manager: boolean in your table. On the basis of this field you can decide whether a user is manager or not. Also you can add validations that manager_id must be blank if manager field is true.
validate :manager_id_blank_for_manager
has_many :employees, class_name: "User", foreign_key: "manager_id", dependent: :restrict_with_exception
scope :managers, ->{ where(manager: true) }
def manager?
manager
end
private
def manager_id_blank_for_manager
if manager_id.present? && manager?
errors.add(:manager_id, :must_be_blank_for_manager) # Move this error to yml file
end
end
I've looked into this SO question but I still have trouble wrapping my head around the concept.
I have a similar setup with the linked SO question, in that I have a User class that contains both Employees and Managers. I also have another model for Role (holding the role names) and UserRole (holding which user has which role).
My requirements state that an Employee (a User whose Role is User) can only have one Manager (a User whose Role is Manager). Now, this Managering concept is an addition to the current system, and I'm not supposed to change the users table, so I'm making a new table with their own MVC.
But now I find it hard to use has_many and belongs_to like the linked question. How do I use the new model? I tried using :through but it doesn't work (for some reason).
Am I doing it wrong? Should I just add a manager_id column to users and work the solution in the linked question into my problem? Also, how do I ensure that only a User whose Role is Manager can be set as a Manager?
Note: I have to say that I'm relatively new to Rails and ActiveRecord, and even Ruby in general.
Note 2: I'm using Rails 4.2.0 if it's relevant.
Setting up a many to many system with roles is pretty straight forward:
class User
has_many :user_roles
has_many :roles, through: :user_roles
def has_role?(role)
roles.where(name: role).any?
end
end
class Role
has_many :user_roles
has_many :users, through: :user_roles
end
class UserRole
belongs_to :role
belongs_to :user
validates_uniqueness_of :role, :user
end
Just make sure you create a unique index on UserRole for role and user:
add_index :user_roles, [:role_id, :user_id], unique: true
The simplest and performant way to implement the manager requirement would be to add a
mananger_id column to users and setup a self-referencing one to many relationship:
class User
has_many :user_roles
has_many :roles, through: :user_roles
belongs_to :manager, class_name: 'User'
has_many :subordinates, foreign_key: :manager_id, class_name: 'User'
validate :authorize_manager!
def has_role?(role)
roles.where(name: role).any?
end
private
def authorize_manager!
if manager.present?
errors.add(:manager, "does not have manager role") unless manager.has_role?("manager")
end
end
end
Another way to do this would be to use resource scoped roles.
The best part is that you don't have build it yourself. There is a excellent gem created by the community called Rolify which sets you up with such as system.
Its also quite a bit more flexible than the former system, once you get a hang of it you can add roles to any kind of resource in your domain.
class User < ActiveRecord::Base
rolify
resourcify
end
---
the_boss = User.find(1)
bob = User.find_by(name: 'Bob')
# creating roles
the_boss.add_role(:manager) # add a global role
the_boss.add_role(:manager, bob) # add a role scoped to a user instance
# querying roles
bob.has_role?(:manager) # => false
the_boss.has_role?(:manager) # => true
the_boss.has_role?(:manager, bob) # => true
the_boss.has_role?(:manager, User.create) # => false
If you go with Rolify compliment it with an authorization library such as Pundit or CanCanCan to enforce the rules.
class User < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :employees, foreign_key: :manager_id, class_name: 'User'
end
Does it helps ?
Update:
class User < ActiveRecord::Base
has_one :role, through: :user_role
belongs_to :manager, -> { where(role: {name: 'manager'}), class_name: 'User'
has_many :employees, -> { where(role: {name: 'employee'}), foreign_key: :manager_id, class_name: 'User'
end
I've been going back and forward on this and I would like some advices.
I have "User" that can be part of many "Organizations", and for each one they can have many "Roles". (actually I have this scenario repeated with other kind of users and with something like roles, but for the sake of the example I summed it up).
My initial approach was doing a Table with user_id, organization_id and role_id, but that would mean many registers with the same user_id and organization_id just to change the role_id.
So I thought of doing an organization_users relation table and an organization_users_roles relation. The thing is, now I don't exactly know how to code the models.
class Organization < ActiveRecord::Base
has_and_belongs_to_many :users, join_table: :organization_users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :organizations, join_table: :organization_users
end
class OrganizationUser < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :organizations
has_many :organization_user_roles
has_many :roles, through: :organization_user_roles
end
class OrganizationUserRole < ActiveRecord::Base
has_and_belongs_to_many :roles
has_and_belongs_to_many :organization_users
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :organization_user_roles
end
If for example I want to get: ´OrganizationUser.first.roles´ I get an error saying: PG::UndefinedTable: ERROR: relation "organization_user_roles" does not exist
How should I fix my models?
You should use a much simpler approach. According to your description, Roles is actually what connects Users to Organizations and vice-versa.
Using the has_many and has_many :through associations, this can be implemented like the following:
class User < ActiveRecord::Base
has_many :roles, inverse_of: :users, dependent: :destroy
has_many :organizations, inverse_of: :users, through: :roles
end
class Organization < ActiveRecord::Base
has_many :roles, inverse_of: :organizations, dependent: :destroy
has_many :users, inverse_of: :organizations, through: :roles
end
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
belongs_to :organization, inverse_of: :roles
end
If you wish to preserve roles when you destroy users or organizations, change the dependent: keys to :nullify. This might be a good idea if you add other descriptive data in your Role and want the role to remain even though temporarily vacated by a user, for example.
The has_many :through association reference:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
To add to jaxx's answer (I upvoted), I originally thought you'd be best looking at has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :positions
has_many :organizations, through: :positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
#columns id | user_id | organization_id | role_id | etc | created_at | updated_at
belongs_to :user
belongs_to :organization
belongs_to :role
delegate :name, to: :role #-> #position.name
end
#app/models/organization.rb
class Organization < ActiveRecord::Base
has_many :positions
has_many :users, through: :positions
end
#app/models/role.rb
class Role < ActiveRecord::Base
has_many :positions
end
This will allow you to call the following:
#organization = Organization.find x
#organization.positions
#organization.users
#user = User.find x
#user.organizations
#user.positions
This is much simpler than your approach, and therefore has much more ability to keep your system flexible & extensible.
If you want to scope your #organizations, you should be able to do so, and still call the users / positions you need.
One of the added benefits of the code above is that the Position model will give you an actual set of data which can be shared between organizations and users.
It resolves one of the main issues with jaxx's answer, which is that you have to set a role for every association you make. With my interpretation, your roles can be set on their own, and each position assigned the privileges each role provides.
If the user can have many Roles for a single organisation,
and OrganizationUser represents this membership,
than, yes, you need another table for organization_user_roles.
You need to explicitly create it in the database (normally with a migration)
To not get confused, try to find a nice name for OrganisationUser, like employment, membership, etc.
I have the following model in my Rails app:
class Course < ActiveRecord::Base
has_many :memberships
has_many :members, through: :memberships, class_name: 'User'
end
While course.members successfully returns the course's members, I don't get access to the Membership model which has a role attribute.
How do I find the user role without having to find the Membership given my Course and User? Can I inject the role attribute to User somehow in the context association?
User model:
class User < ActiveRecord::Base
has_many :memberships, dependent: :destroy
has_many :courses, through: :memberships
end
Here's a solution, not the most beautiful and idiomatic though
class Course < ActiveRecord::Base
has_many :memberships
def members
User.joins(:memberships).where(id: memberships.ids).select('users.*, memberships.role')
end
end
A better approach, suggested through the comments:
has_many :members, -> { select('users.*, memberships.role') }, class_name: 'User', through: :memberships, source: :user
I had the exact same problem, and couldn't find any way to fetch the intermediate association without explicitly finding it. It's not that ugly, though:
class User < ActiveRecord::Base
has_many :memberships, dependent: :destroy
has_many :courses, through: :memberships
def role_in(course)
memberships.find_by!(course: course).role
end
end
user.role_in(course)
# => "teacher"
I think dynamically storing the contextual role depending on what Course is used is not a good idea. It feels hacky to me because the attribute value becomes set implicitly rather than by explicit execution. A User would appear different depending on where and how it's instantiated, despite representing a row in the database.
I would instead implement something like below:
class User
def course_role(course)
memberships.find_by_course(course).role
end
end
class Course
def user_role(user)
members.find_by_user(user).role
end
end
This way, it's explicit that calling methods on either the User or Course to get the role depends on the respective memberships they are associated with.
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 !