I want to make an association where a user has many deals and a deal belongs to a user as well as another with a role ('associate').
I am using the rolify gem to do this.
Like this:
# user.rb
has_many :deals
# deal.rb
belongs_to :user
belongs_to :associate # User with role 'associate or admin'
The first belongs to could be whatever user, doesn't matter if the user is any role it should still work, the second belongs to however should most definitely be an associate or an admin
Do you think I should use rolify for this? or should I not and just make a different model for each role?
Update
My current solution won't work because the user as an associate would need two associations, the has many deals and the has_many :client_deals. I'm not sure in the naming still.
Update 2
Max's solution works great!
This is not where you want to use Rolify's tables. Rolify creates a one-to-one assocation between roles and resources through the roles tables. Roles then have a many-to-many assocation through the users_roles table to users.
Which means it works great for cases where the association is one-to-many or many-to-many but Rolify really can't guarantee that there will ever only be one user with a particular role due to the lack of database constraints.
Even if you add validations or other application level constraints that still leaves the potential for race conditions that could be a double click away.
Instead you want to just create separate one-to-one assocations:
class Deal < ApplicationRecord
belongs_to :creator,
class_name: 'User',
inverse_of: :created_deals
belongs_to :associate,
class_name: 'User',
inverse_of: :deals_as_associate
validates :must_have_associate_role!
private
def must_have_associate_role!
# this could just as well be two separate roles...
errors.add(:associate, '^ user must be an associate!') unless associate.has_role?(:associate)
end
end
class User < ApplicationRecord
has_many :created_deals,
class_name: 'Deal'
foreign_key: :creator_id,
inverse_of: :creator
has_many :deals_as_associate,
class_name: 'Deal'
foreign_key: :associate_id,
inverse_of: :associate
end
Two models can really have an unlimited number of associations between them as long as the name of each assocation is unique and you configure it correctly with the class_name and foreign_key options.
Since this uses a single foreign key this means that ere can ever only be one and you're safeguarded against race conditions.
For associate you can use the following
belongs_to :associate, -> { includes(:roles).where(roles: {name: ['associate', 'admin'] }) }, class_name: 'User', foreign_key: 'user_id'
Related
I am trying to "extend" Rolify functionality to have some global roles such as 'Admin', 'Member', 'Guest', etc... and to be able to set up different "scopes" for each user who have a specific role.
For example, in my app i have this admin role, which is a "super role" meaning it grants access to basically everything. But i also want to be able to "scope" this role for another User, the scope will be, for example 'he will have access to all users, but only if they are from countries A, B, C and from cities X, Y, Z'. I know rolify supports different roles with different scopes, but what i want is to manage "global roles" with different scopes only for different users.
I thought about doing something like a 'Scope' model that belongs to a Role and to a User, in which i would have HABTM relationships with countries and cities, and then use that for authorization (I'm using CanCanCan). But i ran into many issues when working on this approach. It was something like:
class Scope
belongs_to :user
belongs_to :role
has_and_belongs_to_many :countries
has_and_belongs_to_many :cities
end
One of the issues i ran into was that i need to grant the role at the same time i create a scope, and if a user is 'revoked' of a role, the scope which belongs to the user and the role, needs to be destroyed. This last part i found particularly hard since 'Scope' is not related to 'users_roles' table.
Anyone has any idea on a better approach to this problem? I'm having a hard time figuring out the right way to have a role that has custom scopes for each user (basically i need something in the middle of the user and the role to define what is the user's scope with that role).
Appreciate any help I can get!
If you want to create something of your own has_and_belongs_to_many is not the answer (hint: it's almost never the right answer). Using HABTM is the akilles heel of Rolify as its assocations look like this:
class User
has_and_belongs_to_many :roles
end
class Role
has_and_belongs_to_many :users
belongs_to :resource,
polymorphic: true,
optional: true
end
This doesn't let you you query the users_roles table directly or add additional columns or logic. Fixing it has been an open issue since 2013. There are workarounds but Rolify may not be the right tool for the job here anyways.
If you want to roll your own you want to use has_many through: to setup an actual join model so you can query the join table directly and add assocations, additional columns and logic to it.
class User
has_many :user_roles
has_many :roles, through: :user_roles
end
class UserRole
belongs_to :user
belongs_to :role
belongs_to :resource,
polymorphic: true,
optional: true
validates_uniqueness_of :user_id,
scope: [:role_id, :resource_id, :resource_type]
end
class Role
validates :name, presence: true,
uniqueness: true
has_many :user_roles
has_many :roles, through: :user_roles
end
This moves the resource scoping from being per role to being per user.
While you could add additional join tables between the user_roles table and the "scoped" resources its not strictly necissary unless you want to avoid polymorphic asssocations.
Say an admin can be the owner of an account and an account can have many admins. You would have association like the followings :
class Account < ApplicationRecord
belongs_to :account_owner, foreign_key: :account_owner_id, class_name: 'Administrator', optional: true
has_many :administrators
end
class Administrator < ApplicationRecord
# account instance has an account_owner_id that references the administrator "app owner"
has_one :account
# administrator has an account_id on it that references the account he belongs_to
belongs_to :account
That code doesnt work properly because the instance method account defined twice on Administrator clash with each other.
Then I would tend to writte something like this :
class Administrator < ApplicationRecord
has_one :account, as: :proprietary_account
belongs_to :account
to differentiate between the two "sides" of the association.
However this doesnt work either and method proprietary_account is not created
Is there a way to model that with rails associations ? If not how would you create this kind of relationship in a rails app.
You need to create another table called admin_accounts and use the has_many :through association. The admin_accounts table will have two columns, one with admin_id, and one with account_id. You can then associate the two models through that table. More information here: https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
I am building out a rails 5 ruby 2.4.0 app.
I have a Truck model, and a User model.
I would like to reference several users to the truck.. the person who created the truck, the owner of the truck and the driver of the truck if it differs from that of the owner. I want it to reference only the user because I don't want to have may types of user models.
Is this theoretically possible?
im thinking about this:
Truck has_many_users
user belongs_to truck (not sure how to work this?)
when a truck is created it logs the current user id, then the user can select system users for the driver association and owner association...
Please help i'm burning my brain trying to map this out..
So what you're looking for is associating Truck to User multiple times, with each association playing a different role.
It's definitely possible, and quite a common way to set up associations.
However, I would definitely set it up so that the Truck model is the child ("belongs to") the User model, for two reasons -
A truck model should be self-contained in its knowledge. It should know who its creator, driver, and owner are. So you want those id's (foreign key references) stored on the Truck and not the User. Also in your case each truck can have 1 and only 1 creator, driver, or owner. (Side note: If you did have a case where a Truck could have numerous creators, drivers, and owners then this wouldn't work and you'd have to rely on a many-to-many relationship like an intermediary joining table.)
A user can inherently be associated with multiple trucks. A user could own 3 different trucks or they could own 1 truck but drive another. It makes more sense that a User would have the has_many relationships here.
The trick is that ActiveRecord lets you name associations anything you want. So you can try -
class Truck < ApplicationRecord
belongs_to :creator, class_name: "User", foreign_key: :creator_id
belongs_to :owner, class_name: "User", foreign_key: :owner_id
belongs_to :driver, class_name: "User", foreign_key: :driver_id
end
class User < ApplicationRecord
has_many :created_trucks, class_name: "Truck", foreign_key: :creator_id
has_many :owned_trucks, class_name: "Truck", foreign_key: :owner_id
has_many :driven_trucks, class_name: "Truck", foreign_key: :driver_id
end
In each line we override -
The class_name, because if we didn't specify it Rails would use the associated name to guess it. So belongs_to :creator would look for a Creator model, instead of a User model
The foreign_key, because if we didn't specify it Rails would use the associated name to guess it. So belongs_to :creator would look for for a creator_id on this model (since it belongs to the other model). Similarly if we had has_one :creator it would look for a creator_id on the foreign Creator model (or whatever is specified via class_name).
What inverse_of does mean in mongoid associations? What I can get by using it instead of just association without it?
In a simple relation, two models can only be related in a single way, and the name of the relation is automatically the name of the model it is related to. This is fine in most cases, but isn't always enough.
inverse_of allows you to specify the relation you are referring to. This is helpful in cases where you want to use custom names for your relations. For example:
class User
include Mongoid::Document
has_many :requests, class_name: "Request", inverse_of: :requester
has_many :assignments, class_name: "Request", inverse_of: :worker
end
class Request
include Mongoid::Document
belongs_to :requester, class_name: "User", inverse_of: :requests
belongs_to :worker, class_name: "User", inverse_of: :assignments
end
In this example, users can both request and be assigned to tickets. In order to represent these two distinct relationships, we need to define two relations to the same model but with different names. Using inverse_of lets Mongoid know that "requests" goes with "requester" and "assignments" goes with "worker." The advantage here is twofold, we get to use meaningful names for our relation, and we can have two models related in multiple ways. Check out the Mongoid Relations documentation for more detailed information.
Often three (or more) way associations are needed for habtm associations. For instance a permission model with roles.
A particular area of functionality has many users which can access many areas.
The permissions on the area are setup via roles (habtm)
The user/roles association is also habtm
The permissions (read, write, delete, etc) are habtm towards roles.
How would that be best done with rails/activerecord?
I'm not sure if you are just using Role based user permissions as an example, or if that is actually your goal.
Nested habtm relationships are easy in Rails, though I would highly recommend Nested has_many :through, just set them up as you would imagine:
class Permission < ActiveRecord::Base
end
#this table must have permission_id and role_id
class PermissionAssignment < ActiveRecord::Base
belongs_to :permission
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :users, :through => :role_assignments
has_many :permissions, :through => :permission_assignments
end
#this table must have user_id and role_id
class RoleAssignment < ActiveRecord::Base
belongs_to :role
belongs_to :user
end
class User < ActiveRecord::Base
has_many :roles, :through => :role_assignments
end
This question about rails and RBAC (role-based authentication control) has a bunch of useful samples that provide the answer to this question and also sample implementations of your example.