I’m developing a new Rails app and it’s required to have 3 different types of users, lets say user A, user B and user C.
All of them have different properties and different Models depend on each one, but they should also share some of them, like the username property so its possible to refer to the profile via URL (Like on facebook or twitter), or select user’s country, city, etc.
I was wondering if a polymorphic association would be the best approach, creating A, B and C models independently and sharing a Global User model.
class GlobalUser < ActiveRecord::Base
belongs_to :userable, polymorphic: true
end
class UserA < ActiveRecord::Base
has_one :user, as: :userable
end
class UserB < ActiveRecord::Base
has_one :user, as: :userable
end
class UserC < ActiveRecord::Base
has_one :user, as: :userable
end
I’m kind of new to Rails and I’ve never required this type of association before, so any help or suggestion provided will be very helpful.
Is there a better way to do this?
Thanks everybody in advance and sorry for my English
It would help to know the actual types of users in question here. UserA, UserB, and UserC aren’t adding much to the domain knowledge. It’s also unclear what the end goal of having the different types of users is. For the sake of clarifying the conversation some, let’s say you had Student, Instructor and Administrator and the reason you had this was that they would have different types of questions to answer on their user profile.
I would consider keeping a single User model which contains only information necessary to log in and information common to all user types. The user would have a polymorphic profile that contained the unique fields needed for the user types.
For instance:
class User
belongs_to :profile, polymorphic: true
end
class StudentProfile
has_one :user, as: :profile
end
class InstructorProfile
has_one :user, as: :profile
end
class AdminProfile
has_one :user, as: :profile
end
This would load the appropriate profile when you called user.profie. It makes some assumptions that may or may not be safe to make - for instance, that a user can’t be two of those types.
Thanks for the clarification @derekprior. I ran into a similar situation recently where I was trying to build a marketplace site with two types of User (Buyer and Seller) but just wanted one login mechanism to authenticate User with.
I was wondering in your example, say when you need to use some information from a profile, (i.e. User show page) would you call something like current_user.profile.grade (grade being some information only relevant to Student). How would you get around the law of Demeter?
I originally thought about delegating all the methods in User to the relevant profile in this case, but that probably would leave you with a rather bloated User class. I suppose you could use a Presenter here?
The other idea I had was to re-write the current_user method so you’d have something like:
def current_student
if current_user
@current_student ||= StudentProfile.where(user: current_user).first
end
end
And then in StudentProfile delegate stuff like name, email, password etc to User so you could basically have a fully functioning ‘student user’ if that makes sense.
Am I totally off in this case? I actually scrapped the above and simply ended up having a buyer and seller boolean on User to keep things simple to start with in my project, but I’d be interested to hear your thoughts with regard to your example above.
Personally I would go with the polymorphic relationship, or forgo multiple user ‘types’ completely and go for composition. Put your behavior in modules instead of into inheritance structures.
The biggest problem with figuring out where to put things, is most people dump shit tons into the user model that shouldn’t be there. Have billing info? Make it its own object. An address? It’s own object. You get the idea.
Put what a seller needs in the Seller module and the buyer stuff in the Buyer module. These are essentially going to map to roles for the user.
Keep to 1 user type in rails or you will cry many tears
Yes I tend to agree with you @Dreamr. I think there are naturally some shared attributes between the different types of Users and so my preference would be to go polymorphic. Forgoing types and adopting ‘full-blown’ composition could work but will be quite a bit more work setting it up IMO.
One case I’ve thought about recently, which I think you touched upon, is say you have a large User model which has attributes like billing info, address, profile information, preferences etc - all saved in the User table.
If your ActiveRecord::Base classes are merely responsible for dealing with persistence etc then what’s the argument against saving all the attributes related to User in one large User table and then creating lots of small, service type objects and using dependency injection when you need to do something with that data?
Put another way, what’s the benefit for breaking up your large ActiveRecord::Base class into lots of smaller ActiveRecord::Base classes if all they’re doing is dealing with persistence anyway related to one central/parent object (User)?
For me it makes sense to break things up and use relationships to connect the different objects together, but I think it’s worth asking the question why (given the example above)?
I feel like this is turning into an argument for document based storage though I don’t know enough about it to comment, other than I should stay away from it apparently!
PS Nice work with your blog. Saw it on Ruby Weekly and then realized it was you on here!
“If your ActiveRecord::Base classes are merely responsible for dealing with persistence etc then what’s the argument against saving all the attributes related to User in one large User table and then creating lots of small, service type objects and using dependency injection when you need to do something with that data?”
Excellent question… My answer is I have a major distaste for nils, be they in me database, or be they in me code.
A table with 30 columns that typically only uses a subset of that data becomes unwieldily fast. It is a much better practice to factor our and normalize that data into multiple tables where the objects only know about the fields they need.
Writing conditional logic is a most hated pastime of mine, and this kind of database design beggs for conditional logic in my opinion.
Thanks for the blog congrats I have a lot of fun sharing my vision of Ruby with the world.
Thanks @Dreamr. Some useful insight there. I like the point you raise about not having nils in your database.
I came across a few articles concerning data normalization which lead me to reading about Third Normal Form (3NF). Together with First and Second Normal Form, these appear to lay a good foundation for setting out your database tables, and when applied they seem to avoid what I suggest in my original post (having one large User table).
(I didn’t do a CS degree so this stuff is all new to me!)
@georgebrock also highlighted in our mentor exchange, how separating the large table into smaller tables will lead to less coupling and more explicitness in our code.