8

So, I have the following:

class Product < ActiveRecord::Base
  # Has a bunch of common stuff about assembly hierarchy, etc
end

class SpecializedProduct < Product
  # Has some special stuff that a "Product" can not do!
end

There's a manufacturing and assembly process in which data is captured about Products. At the time of capture the eventual product type is not known. after the Product record has been created in the database (perhaps days later) it may be necessary to turn that product into a specialized product and fill in the additional information. Not all products will become specialized, however.

I've been trying to use the following:

object_to_change = Product.find(params[:id])
object_to_change.becomes SpecializedProduct
object_to_change.save

Then, when I do a SpecializedProduct.all the resulting set does not include object_to_change. Instead object_to_change is still listed in the database as Product

UPDATE "products" SET "type" = ?, "updated_at" = ? WHERE "products"."type" IN ('SpecializedProduct') AND "products"."id" = 30  [["type", "Product"], ["updated_at", Fri, 17 May 2013 10:28:06 UTC +00:00]]

So, after the call to .becomes SpecializedProduct the .save method is now using the right type, but it's not able to update the record because the WHERE clause of the update is too specific.

Do I really need to access the type attribute of the model directly? I'd really rather not.

Jamie
  • 93
  • 1
  • 4

4 Answers4

2

Looking at the source of becomes and becomes!, it doesn't mutate the original object. You need to assign it to a new variable:

some_product = Product.find(params[:id])
specialized_product = some_product.becomes SpecializedProduct
specialized_product.save

Not sure how this will handle the primary key of the record, though, so you may need to do some additional finagling to make sure your relations don't get mangled.

lobati
  • 9,284
  • 5
  • 40
  • 61
2

You just need the bang version of the becomes method with (!) and save.

The difference between the two methods: becomes creates a new instance of the new class with all the same attribute values of the original object. becomes! also updates the type column.

object_to_change = Product.find(params[:id])
object_to_change.becomes! SpecializedProduct
object_to_change.save
Mark Swardstrom
  • 17,217
  • 6
  • 62
  • 70
0

I think no! Check the sorces of becomes and becomes! methods! https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L199

Looks like you need to use becomes! because it is wrapper around becomes that also changes the instance's sti column value.

UPD: https://github.com/rails/rails/blob/4-0-stable/activerecord/test/cases/persistence_test.rb#L279 This is a test case for your code.

UPD2: I think you can try to create another class sort of DefaultProject which is subclass of Project and then you can change each from DefaultProject to SpecializedProduct and vice versa

Fivell
  • 11,829
  • 3
  • 61
  • 99
  • 1
    I get the same resulting SQL on the rails console with `.becomes!` as I do with `.becomes` `SET "type" = ?` is still in there, but unfortunately so is `"products"."type" IN ('SpecializedProduct')` in the `WHERE` clause. – Jamie May 20 '13 at 19:32
  • see my updated answer, it should work...what rails version are you using ? – Fivell May 21 '13 at 05:13
0

I have a similar issue where I want to change from one subclass to another. Unfortunately Rails doesn't do this gracefully because it wants to limit the save with a "where type='NewSubclass'". ie:

UPDATE parents SET type='NewSubclass' WHERE type IN 'NewSubclass' AND id=1234

Digging into rails, it appears that the method in ActiveRecord's lib/active_record/inheritance.rb named "finder_needs_type_condition?" is called and the caller isn't smart enough to realize you are changing the type field so it obviously isn't already that value.

I "resolved" this in a round about way. I used the ActiveRecord core as a basis for how to load an instance of whichever class I want with attributes and save it without having to go through the full ActiveRecord find stack.

old_instance = Parent.find(id) #returns OldSubclass instance
tmp = Parent.allocate.init_with('attributes' => old_instance.attributes)
tmp.type = 'NewSubclass'
tmp.save
Parent.find(id) #returns NewSubclass instance

It's real ugly and I hate it. Hopefully someone will think about fixing this in ActiveRecord. I believe it would be useful for objects to change subclasses in STI over time. I have a single table with 5 subclasses because it cleans up the model quite a bit.

This is the only ugliness I had to live with. Be sure to write proper tests so that when ActiveRecord breaks this "workaround" you can detect it.

timd
  • 1