Topic: Should I use STI?

According to DHH's world of resources pdf, one should model beyond things with:

1.  Relations
2.  Events
3.  State

The example usage in the PDF is state similar to the following.

class kase
  has_one :progress
end

class Progress
  belongs_to :user
  belongs_to :kase
end

class Opened < Progress
end

class Reviewed < Progress
end

class Closed < Progress
end


To me this looks like the usage of Single Table Inheritance to accomplish this.

I have something I have been trying to model that seems like it could be a good candidate for STI and seems similar to what I mention above, but the view requires a drop down list to choose a type.

here is what I would like to do:

class User
  has_one :permission
end

class Permission
  belongs_to :user
end

class Guest < Permission
end

class Member < Permission
end

class Admin < Permission
end


On the new User page, I would like a drop down list to appear that allows the user to choose a permission type.  So creating multiple models in one form. 

For some reason, I cannot get the create action to recognize the fact that a given type of user is created.  It only creates a Permission.new, even if I manually set @permission[:type] to the form parameter. 

  # POST /users
  # POST /users.xml
  def create
    @user = User.new(params[:user])
    @permission = Permission.create(params[:permission])
    @permission[:type] = params[:type]
   
    User.transaction do
      @permission.user = @user
      @permission[:type] = params[:type]
      @user.save!
      @permission.save!
      redirect_to :action => :show, :id => @user
    end
  end

Why does my create action not work????

Thanks in advance to anyone who can offer some advice on how to solve this.

Last edited by lambo4jos (2007-06-11 22:36:03)

Re: Should I use STI?

Imagine you have a class called Automobile, which has a "make" attribute, among others. You inherit from that class to make a class called Chrysler. Class Chrysler automatically sets the "make" attribute to "chrysler", so you don't have to do it all the time. Now imagine you say:

my_car = Automobile.new
my_car.make = "chrysler"

What will the class of the my_car object be? It will be of class Automobile. if you don't get that, please say so. I will assume for now that you understand your problem now and present you with a solution:

def create
  # if params[:permission][:type] is "Guest", the following
  # line will be like doing "@user = Guest.new"
  @user = params[:permission][:type].constantize.new
  @permission = @user.build_permission(params[:permission])
  @user.save!
  @permission.save!
  redirect_to :action => :show, :id => @user
end

And that's why ruby is the bestest.

Last edited by fabio (2007-06-11 02:57:12)

Re: Should I use STI?

Thanks for your response.

I now understand I must do Guest.new or Member.new.  Thanks for the insight.  I ran into errors with the presented solution and I don't know why.  I cut and paste this code exactly and put it in my Users controller create action and it did not work.

Here is my error message

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]

But the request parameters look right.

Request

Parameters: {"user"=>{"name"=>"fred", "created_at(1i)"=>"2007", "created_at(2i)"=>"6", "created_at(3i)"=>"11", "created_at(4i)"=>"00", "created_at(5i)"=>"18"}, "commit"=>"Create", "permissions"=>{"type"=>"Guest"}}

Just a reminder.  Guest, Member, and Admin are inheriting from Permissions, and not Users. 

I am getting the error with the code on this line,

params[:permission][:type]

and it has happened with other solutions I have tried even before using the constantize method.  Does it have something to do with creating mutliple models in one form?

Any ideas?

Re: Should I use STI?

I think this might just be a simple typo.

params[:permission][:type]
# should be
params[:permissions][:type]

my bad.

Re: Should I use STI?

Almost there!  Something worked, but not completely yet.

here is the error now.

undefined method `build_permission' for #<Guest:0x4830104>

  def create
   # if params[:permission][:type] is "Guest", the following
   # line will be like doing "@user = Guest.new"
   @user = params[:permissions][:type].constantize.new
   @permission = @user.build_permission(params[:permission])
   @user.save!
   @permission.save!
   redirect_to :action => :show, :id => @user
  end

Parameters: {"user"=>{"name"=>"joe", "created_at(1i)"=>"2007", "created_at(2i)"=>"6", "created_at(3i)"=>"11", "created_at(4i)"=>"00", "created_at(5i)"=>"44"}, "commit"=>"Create", "permissions"=>{"type"=>"Guest"}}

Last edited by lambo4jos (2007-06-11 22:33:56)

Re: Should I use STI?

Oh, this is because of my original mistake in thinking that class Guest et. al. were inheriting from User, but they are inheriting from Permission. So we are trying to do Permission#build_permission, which doesn't exist. Try this:

def create
  # if params[:permissions][:type] is "Guest", the following
  # line will be like doing "@permission = Guest.new"
  @permission = params[:permissions][:type].constantize.new(params[:permissions])
  @user = @permission.build_user(params[:user])
  # We shouldn't need to save the user, @permission.save should save both
  #@user.save!
  @permission.save!
  redirect_to :action => :show, :id => @user
end

Re: Should I use STI?

I tried using the code and it sets the users type to permission and not the param thats in the form like it should.  Not sure why. 

What about changing the User belongs_to :permission, and Permission has_many :users to get the build_permission?
Unless it can be solved as is of course?

Re: Should I use STI?

How about this?:

def create
  @permission = params[:permissions][:type].constantize.new
  @user = @permission.build_user(params[:user])
  @permission.save!
  redirect_to :action => :show, :id => @user
end

Question: are there other attributes to the Permission objects? Other than "type"?

btw, we already have User#build_permission. You could write the same thing as above in a different way:

def create
  @user = User.new(params[:user])
  @user.permission = params[:permissions][:type].constantize.new
  @permission.save!
  redirect_to :action => :show, :id => @user
end

Re: Should I use STI?

So the first option you have listed works, but for some reason the second one didn't for me. 

fabio wrote:

Question: are there other attributes to the Permission objects? Other than "type"?

As of now, no and I don't think there will be.  Does this matter?

I see you posted a solution that you removed and replaced with the current one here.  I am referring to the one where you mention to have a UserType table populated with the 3 attributes and the map method you use to display them in the form.  I am pretty sure I can solve that on my own, but which would you recommend?

What are your design recommendations as far as STI, vs. the other method you have removed.  A problem I see with the STI method I chose now is that when I update, I don't know how to display the users current permission type in the edit form select list.  Because I specify the select list options in an array, it defaults to Guest, even if the user is an admin, but this is only for the edit form.  Otherwise the users correct permission is in the DB.


The reason I seperated permissions because they are totally seperate from types of Users.  I didn't want to inherit from the Users table, because i may need inheritance in a different form for the Users table that may require different attributes for different users, such as customer, and employee.  The permissions table will just be used for what actions a User can perform and what views will be available.

Thanks for your valuable help.  Any thoughts would be appreciated.