I've been banging my head against a wall trying to wrap my head around this, so any guidance would be much appreciated...
I want to have a User system setup to reflect the following hierarchy:
User
|- email address
|- password
|- billing information
|- contact information
|- account preferences
|
|=> Agent
|=> - agent-specific information
|=> - has_many Users
|=> - belongs_to Manager
|
|=> Manager
|=> - manager-specific information
|=> - has_many Agents, Users
|
|=> Administrator
|=> - can manage everything
I already have a User
model with Devise and CanCan setup to handle authentication and authorization, so I know how to use roles to restrict the type of user to specific actions, and so forth.
What I'm lost at is how to organize these sub-class relationships both in my Rails code and in the database. As you can see from above, Agent
, Manager
, and Administrator
all share the information contained in User
, but each has additional functionality AND information associated with it.
I've read some about STI, polymorphic associations, and self-referential associations.
If I use STI, the User
table would have to contain fields for all of my [Agent
/Manager
/Administrator
]-specific information, right? That would make my User
table huge, which is something I'd like to avoid. Conversely, if I use polymorphic, then wouldn't I have to duplicate all the common information in User
across all the other types of User
subclass tables?
And to add to my confusion, I can't wrap my head around how the answer to the above question would work with the relationships between the subclasses (as in, that a Manager
has_many Agents
, but both are subclasses of User
...??).
I would really appreciate someone setting me straight on this through a detailed answer that gives due consideration to code readability and data integrity, that explains simply (as if to a Rails newbie) why A is the best approach and why B or n is--by comparison--not a good approach for this situation, and that gives example code to implement the relationships described above. I want to solve this problem, but more importantly, I want to learn why the solution works!
Single Table Inheritance (STI) models are defined as separate classes inheriting from one base class, but they aren't associated with separate tables — they share one database table. The table contains a type column that defines which subclass an object belongs to.
Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.
In Single-Table Inheritance (STI), many subclasses inherit from one superclass with all the data in the same table in the database. The superclass has a “type” column to determine which subclass an object belongs to. In a polymorphic association, one model “belongs to” several other models using a single association.
I don't think there is a simple explanation why any approach is the best in any circumstance. A significant amount of questions on Stack Overflow are question on how to design relations between their models. It's a complicated topic and the right solutions require intimate knowledge of the problem you are solving. And even then, you probably won't get it right the first couple of times.
The best way so far is to completely work TDD/BDD on this matter and let the tests/specs drive out your design. And don't be afraid to refactor is you find a better way. Most of the time you will only see the right solution after you've tried a couple of wrong ones. You'll get to know the edge cases. As Ward Cunningham puts it in his "Technical Debt" analogy: "Refactor afterwards as if you knew what you were doing from the start". Be sure to have the acceptance tests to verify its behavior afterwards though.
Getting more specific to your problem. There is a third option and that is to completely split up the classes, each with their own table. I've tried it in my current project and I like it. You don't need to define something like a user, if that doesn't make sense in your business domain. If they have shared behavior, use mixins. The most important caveat is that it's not straight forward to have them login through the same form anymore.
I have Admin, Recruiter, Supplier and Visistor models. They are all separate models, sharing some behavior with mixins. They all have their own namespaced controllers to act on. For instance, all actions for the admins are in the Backend namespace. There is a namespaced ApplicationController too. The Backend::ApplicationController simply specifies before_filter :authorize_admin!
. No switching, no complicated case statements, nothing.
You need to pay special attention to conventions. If you use the same names across the models, your mixins can become super easy. Read up on ActiveSupport::Concern to make mixins even easier to work with. I have a mixin like this:
module Account
extend ActiveSupport::Concern
included do
devise :database_authenticatable, :trackable, :recoverable, :rememberable
end
end
And in my routes:
devise_for :recruiters
devise_for :suppliers
# etc...
And the app/controllers/backend/application_controller.rb
looks like this:
class Backend::ApplicationController < ::ApplicationController
layout "backend"
before_filter :authenticate_admin!
def current_ability
@current_ability ||= AdminAbility.new(current_admin)
end
end
So, to conclude. Any architecture will work. STI and polymorphism have their place, but be sure to model your architecture according to your domain. Ruby is a very flexible language and you can use this to your advantage. Devise and CanCan are excellent gems and can handle these situations with ease. I showed you my solution for a project I'm currently working on. It works well for me, but I cannot say if it works right for you. Don't be afraid to experiment and refactor when you feel you've made a bad decision, rather than keep patching up your original idea.
PS. Speaking of STI and relations: they work excellent together too. You can define relations from one subclass to another subclasses. It will all work as expected.
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