Rails: PolyMorphic or STI or something else for User management?
Asked Answered
C

2

6

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!

Civilly answered 11/12, 2010 at 16:28 Comment(0)
N
8

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.

Nosebleed answered 11/12, 2010 at 17:25 Comment(6)
I read several other articles referencing Modules and mixins, yet I don't have any idea what they are. Is there a good resource online that you know of where I can read up on what they are and how to use them? I couldn't glean much from the official Rails docs...Civilly
Modules aren't Rails specific, so they are not actively mentioned in the Rails docs. Any documentation on Ruby metaprogramming will mention them extensively, because they are an important part of Ruby. I learned the most from Dave Thomas' screencasts. Not only good for learning Ruby, but learning OO in general.Nosebleed
Thanks; I'll definitely give those a look!Civilly
I've been reading up on Modules and mixins--courtesy of the Picaxe book--but all the examples I've found thus far talk about Modules and mixins in a pure Ruby environment, not within Rails. Where do I define Modules in Rails?Civilly
Great answer! Mixins are underused.Pettigrew
@Civilly wherever you want. Most people create a directory called app/concerns or app/behavior or app/traits, or use the lib-directory. If you go outside your app-directory, you have to specify autoload_paths in config/application.rb.Nosebleed
H
0

It may be tempting to over-think this, but I'm not convinced what you want to do is very complicated.

STI is probably not the best idea here, because you restrict a user to no more than one of these key roles.

As far as storing membership specific information, I'm not sure that your tables will be "huge" when you add columns for STI. If they are null, they aren't really consuming any resources. If you don't need to select on that data, you might consider having a single column role_information and then using serialize :role_information so that you can define a hash and store it in the column.

Another option is to override methods such that they check for role membership when responding, i.e.:

has_many :agents
def agents
  return nil unless self.has_role(:manager) 
  return self[:agents]
end

Replace "has_role" with whatever you need to check role membership.

You can do this on the setting side as well.

Authorization for specific functions should be done in your controller, so you should check for role membership at that point.

Horizontal answered 11/12, 2010 at 17:31 Comment(1)
This will create a huge amount of if/unless statements all over your code. This is difficult to test and can lead to a lot of complexity and bugs. Try not to make classes that represent more than one thing.Nosebleed

© 2022 - 2024 — McMap. All rights reserved.