STI and change the type

Hey,

I'm trying to use STI in my models, while writing a little cms. A have a model called Page and some subclasses like ContentPage, Sysfolder etc. Some classes share similar attributes, so I use STI and one table for all types. Now I'm writing a form for edititing a page and I want to have the opportunity to change the type of it at runtime. But that seems to be a problem:

When I do:

page = Page.find(x) # page.type is Sysfolder page.update_attributes(params[:record]) # with params[:record][:type] = ContentPage

or additionally

page.type = ContentPage # or page[:type] = ContentPage page.save!

everything is saved, but not the type. When I try to debug the class of page I get Sysfolder (thanks to STI) and I think here lies the problem, because I cannot change the class at runtime. I tried a lot af things like creating a new page and try to copy it over, but none worked.

Is there a workaround or option I do not see right now?

Regards,

Mike

Mike wrote:

Hey,

I'm trying to use STI in my models, while writing a little cms. A have a model called Page and some subclasses like ContentPage, Sysfolder etc. Some classes share similar attributes, so I use STI and one table for all types. Now I'm writing a form for edititing a page and I want to have the opportunity to change the type of it at runtime. But that seems to be a problem:

When I do:

page = Page.find(x) # page.type is Sysfolder page.update_attributes(params[:record]) # with params[:record][:type] = ContentPage

or additionally

page.type = ContentPage # or page[:type] = ContentPage page.save!

everything is saved, but not the type. When I try to debug the class of page I get Sysfolder (thanks to STI) and I think here lies the problem, because I cannot change the class at runtime. I tried a lot af things like creating a new page and try to copy it over, but none worked.

Is there a workaround or option I do not see right now?

Regards,

Mike

>

Try page[:type] = "ContentPage"

Hope this helps Chris

As you can see in my first post (commented out), I already tried this one out, but it's not working. The problem seems to be that when I say page = Page.find (x), the class of page isn't "Page", it's Sysfolder instead (if the "type" of page is Sysfolder) and I'am not able to transform a Sysfolder into another subclass of page (why?). The same problem seems to affect the migration too: By defining a default value for the type like:

t.column :type, :string, :default => 'Sysfolder'

it's not possible to use the create-method on Page with another type. As a result of the create-method, I always get a Sysfolder! I don't understand, why ActiveRecord doesn't let me change the type in a normal way!

Does anybody found a work around for this problem? I don't need to use the create-method, but a manual change of the type/class would help me a lot. Maybe I'll give it a try to use pure SQL to change the type.

Thanks in advance.

Mike

The issues you encounter are by design. Ruby will not let you change the class of an object underneath it (unless using evil.rb: http://rubyforge.org/projects/evil). Changing the type attribute of an instantiated ActiveRecord object is also generally bad practice. Consider this scenario:

  class Page < ActiveRecord::Base     validates_presence_of :title   end

  class SummaryPage < Page     validates_presence_of :summary   end

  page = Page.find(:first)   page.summary = nil   page[:type] = 'SummaryPage'   page.save!

This object will pass all validations and save just fine, even though the intent was to change the type to 'SummaryPage', which requires a 'summary' attribute that is nil in this case. Alternative solutions could be:

  page = Page.find(:first)   summary_page = SummaryPage.new(page.attributes)

You could also write a method that looks up the right class based on a :type attribute and instantiates a new object based on that, and so forth. In ActiveRecord STI, your 'default' class should be the one that maps to the table itself, not the one defined by the default value of the 'type' column.

- Gabriel

Gabriel Gironda wrote:

The issues you encounter are by design. Ruby will not let you change the class of an object underneath it (unless using evil.rb: http://rubyforge.org/projects/evil). Changing the type attribute of an instantiated ActiveRecord object is also generally bad practice. Consider this scenario:

  class Page < ActiveRecord::Base     validates_presence_of :title   end

  class SummaryPage < Page     validates_presence_of :summary   end

  page = Page.find(:first)   page.summary = nil   page[:type] = 'SummaryPage'   page.save!

This object will pass all validations and save just fine, even though the intent was to change the type to 'SummaryPage', which requires a 'summary' attribute that is nil in this case. Alternative solutions could be:

  page = Page.find(:first)   summary_page = SummaryPage.new(page.attributes)

You could also write a method that looks up the right class based on a :type attribute and instantiates a new object based on that, and so forth. In ActiveRecord STI, your 'default' class should be the one that maps to the table itself, not the one defined by the default value of the 'type' column.

- Gabriel

As you can see in my first post (commented out), I already tried this one out, but it's not working. The problem seems to be that when I say page = Page.find (x), the class of page isn't "Page", it's Sysfolder instead (if the "type" of page is Sysfolder) and I'am not able to transform a Sysfolder into another subclass of page (why?). The same problem seems to affect the migration too: By defining a default value for the type like:

t.column :type, :string, :default => 'Sysfolder'

it's not possible to use the create-method on Page with another type. As a result of the create-method, I always get a Sysfolder! I don't understand, why ActiveRecord doesn't let me change the type in a normal way!

Does anybody found a work around for this problem? I don't need to use the create-method, but a manual change of the type/class would help me a lot. Maybe I'll give it a try to use pure SQL to change the type.

Thanks in advance.

Mike

>

I believe you can change the type manually as I had it in my post (providing it passes validation) -- at last it seems to work in the console. However, you will need to instantiate the item again as reload won't work, as it's looking for the old "type". However, I agree it is bad practice.

Cheers Chris

I think we're defining "type" differently. You can change the type attribute, using the hash accessors, but the object's class is still the same one it was instantiated with - when you save the object you may have bypassed validations and callbacks and so forth that are defined on the class you're trying to change to.

Reload won't work not because it's looking for the old type, but because you can't change the value of "self" - and once you do reinstantiate from the database, your object is potentially invalid because it was saved as an instance of a different AR derived class than the 'type' column reflects.

- Gabriel

I finally did it the way you described in your first post.

page = Page.find(:first) summary_page = SummaryPage.new(page.attributes)

The problem I got here was, that after saving summary_page I had a new record in the database. So I extended ActiveRecord::Base with a new_record= method and now I can set this flag manually. Maybe it's not the best way to do this, but for now it works.

After working for a month with Rails now I am still impressed by its features, but I more and more find restrictions to deal with. Well... I think that's the price to pay for beeing "conventional".

Thanks for your help anyway!

Gabriel Gironda wrote:

I think we're defining "type" differently. You can change the type attribute, using the hash accessors, but the object's class is still the same one it was instantiated with - when you save the object you may have bypassed validations and callbacks and so forth that are defined on the class you're trying to change to.

Yes, we're both saying the same thing here.

Mike wrote:

I finally did it the way you described in your first post.

page = Page.find(:first) summary_page = SummaryPage.new(page.attributes)

Be aware that protected attributes won't be copied.

So if you are using attr_protected or attr_accessible, you'll have to write a variant of AR's clone method that is able to instantiate an object of a different class.