I have a rails application that has three different types of users and I need them all to share the same common profile information. However, each different user also has unique attributes themselves. I'm not sure how to separate out the different fields.
I'm using devise for authentication and cancan for authorization. Therefore I have a User model with a set of roles that can be applied to the user. This class looks this:
class User < ActiveRecord::Base
# ... devise stuff omitted for brevity ...
# Roles association
has_many :assignments
has_many :roles, :through => :assignments
# For cancan: https://github.com/ryanb/cancan/wiki/Separate-Role-Model
def has_role?(role_sym)
roles.any? { |r| r.name.underscore.to_sym == role_sym }
end
end
Each user has a profile that includes:
I do not want to pollute the User model with this info so I'm throwing it into a Profile model. This part is fairly simple. This turns the User model into something like this:
class User < ActiveRecord::Base
# ... devise stuff omitted for brevity ...
# ... cancan stuff omitted for brevity ...
has_one :profile
end
The additional fields is where I have some uneasy feelings about how to model are ...
If a user is an admin, they'll have unique fields such as:
If a user is a Owner they'll have unique fields ...
If a user is a member they'll have a few additional fields as such:
...
and a Store model will contain a has_many on the members so we work the the members of the store.
The issue is around the additional fields. Do I set these up as different classes? Put them into a different I've currently tried a few different ways to set this up:
One way is to set up the User Model as aggregate root
class User < ActiveRecord::Base
# ...
# Roles association
has_many :assignments
has_many :roles, :through => :assignments
# Profile and other object types
has_one :profile
has_one :admin
has_one :owner
has_one :member
# ...
end
The benefit of this approach is the User model is the root and can access everything. The downfall is that if the user is a "owner" then the "admin" and "member" references will be nil (and the cartesian of the other possibilities - admin but not owner or member, etc).
The other option I was thinking of was to have each type of user inherit from the User model as such:
class User < ActiveRecord::Base
# ... other code removed for brevity
has_one :profile
end
class Admin < User
# admin fields
end
class Owner < User
# owner fields
end
class Member < User
# member fields
end
Problem with this is that I'm polluting the User object with all kinds of nil's in the table where one type doesn't need the values from another type/etc. Just seems kind of messy, but I'm not sure.
The other option was to create each account type as the root, but have the user as a child object as shown below.
class Admin
has_one :user
# admin fields go here.
end
class Owner
has_one :user
# owner fields go here.
end
class Member
has_one :user
# member fields go here.
end
The problem with the above is I'm not sure how to load up the proper class once the user logs in. I'll have their user_id and I'll be able to tell which role they are (because of the role association on the user model), but I'm not sure how to go from user UP to a root object. Methods? other?
Conclusion I have a few different ways to do this, but I'm not sure what the correct "rails" approach is. What is the correct way to model this in rails AR? (MySQL backend). If there is not a "right" approach, whats the best of the above (I'm also open to other ideas).
Thanks!
My answer assumes that a given user can only be one type of user - e.g. ONLY an Admin or ONLY a Member. If so, this seems like a perfect job for ActiveRecord's Polymorphic association.
class User < ActiveRecord::Base
# ...
belongs_to :privilege, :polymorphic => true
end
This association gives User an accessor called 'privilege' (for lack of a better term and to avoid naming confusion which will become apparent later). Because it is polymorphic, it can return a variety of classes. The polymorphic relationship requires two columns on the corresponding table - one (accessor)_type
and (accessor)_id
. In my example, the User table would gain two fields: privilege_type
and privilege_id
which ActiveRecord combines to find the associated entry during lookups.
Your Admin, Owner and Member classes look like this:
class Admin
has_one :user, :as => :privilege
# admin fields go here.
end
class Owner
has_one :user, :as => :privilege
# owner fields go here.
end
class Member
has_one :user, :as => :privilege
# member fields go here.
end
Now you can do things like this:
u = User.new(:attribute1 => user_val1, ...)
u.privilege = Admin.new(:admin_att1 => :admin_val1, ...)
u.save!
# Saves a new user (#3, for example) and a new
# admin entry (#2 in my pretend world).
u.privilege_type # Returns 'Admin'
u.privilege_id # Returns 2
u.privilege # returns the Admin#2 instance.
# ActiveRecord's SQL behind the scenes:
# SELECT * FROM admin WHERE id=2
u.privilege.is_a? Admin # returns true
u.privilege.is_a? Member # returns false
Admin.find(2).user # returns User#3
# ActiveRecord's SQL behind the scenes:
# SELECT * FROM user WHERE privilege_type='Admin'
# AND privilege_id=2
I would recommend you make the (accessor)_type
field on the database an ENUM if you expect it to be a known set of values. An ENUM, IMHO, is a better choice than a VARCHAR255 which Rails would normally default to, is easier/faster/smaller to index but makes changes down the road more difficult/timeconsuming when you've got millions of users. Also, index the association properly:
add_column :privilege_type, "ENUM('Admin','Owner','Member')", :null => false
add_column :privilege_id, :integer, :null => false
add_index :user, [:privilege_type, :privilege_id], :unique => true
add_index :user, :privilege_type
The first index allows ActiveRecord to rapidly find the reverse association (e.g. find the user that has a privilege of Admin#2) and the second index allows you to find all Admins or all Members.
This RailsCast is a bit dated but a good tutorial on polymorphic relationships nonetheless.
One last note - in your question, you indicated Admin, Owner or Member was the user's type which is appropriate enough, but as you probably see, I'd have to explain that your user table would then have a user_type_type
field.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With