Rob Johnson
Rob Johnson

Reputation: 3

Converting Single Table Inheritence Models using ActiveRecord::Persistence#becomes

I'm using Single Table Inheritance in a Rails project and attempting to change the type of one model to that of another. Here are the relevant schema and models:

create_table "images", force: true do |t|                                                                                                                                          
  t.string   "url"                                                                                                                                                                  
  t.string   "type"                                                                                                                                                                  
  t.datetime "created_at"                                                                                                                                                          
  t.datetime "updated_at"                                                                                                                                                            
  t.integer  "user_id",    limit: 255                                                                                                                                                 
end


class Image < ActiveRecord::Base
  validates :url, :user_id, presence: true
end

class UnconfirmedImage < Image
end

class ConfirmedImage < Image
end

I need to convert an UnconfirmedImage to a ConfirmedImage and vice versa. I should be able to do this using ActiveRecord::Persistance#becomes!.

However, when I attempt to save the change, it seems to fail silently:

foo = UnconfirmedImage.new(url: "foo", user_id:1)
=> #<UnconfirmedImage id: nil, url: "foo", type: "UnconfirmedImage", created_at: nil,    updated_at: nil, user_id: 1>
foo.save
#sql omitted                                                                                                                  
=> true


bar = foo.becomes!(ConfirmedImage)                                                                                                                                      
=> #<ConfirmedImage id: nil, url: "foo", type: "ConfirmedImage", created_at: nil,  updated_at: nil, user_id: 1>

bar.save

Note the wrong sql generated here. The WHERE clause on type checks for the new type, rather than the old. This shouldn't return true.

[13891][12:32:05.583 +0000][DEBUG]:    (0.1ms)  begin transaction 
[13891][12:32:05.598 +0000][DEBUG]:   SQL (0.3ms)  UPDATE "images" SET "type" = ?,"updated_at" = ? WHERE "images"."type" IN ('ConfirmedImage') AND "images"."id" = 2 [["type", "ConfirmedImage"], ["updated_at", Mon, 17 Feb 2014 12:32:05 UTC +00:00]]                                                                                                                         
[13891][12:32:05.599 +0000][DEBUG]:    (0.1ms)  commit transaction                                                                                                                      
=> true

This is confirmed when I attempt to query for the object.

UnconfirmedImage.all
[13891][12:33:59.525 +0000][DEBUG]:   UnconfirmedImage Load (0.3ms)  SELECT "images".*   FROM "images" WHERE "images"."type" IN ('UnconfirmedImage')
=> #<UnconfirmedImage id: 2, url: "foo", type: "UnconfirmedImage", created_at: "2014-02- 17 12:31:15", updated_at: "2014-02-17 12:31:15", user_id: 1>]>

ConfirmedImage.all
[13891][12:33:39.646 +0000][DEBUG]:   ConfirmedImage Load (0.2ms)  SELECT "images".* FROM "images" WHERE "images"."type" IN ('ConfirmedImage')
 => #<ActiveRecord::Relation []>

Could anyone advise the best solution to this? I'm not sure if this is expected behaviour, or a bug in Rails.

Thanks

Upvotes: 0

Views: 466

Answers (1)

Kenneth Kalmer
Kenneth Kalmer

Reputation: 341

This has bitten me before many times. Somewhere ActiveRecord or ARel hangs onto the old type. The way I've gotten around it in the past is by simply doing something like this:

image = image.becomes(ConfirmedImage)
Image.where(id: image.id).update_all(type: 'ConfirmedImage')

So later when the queries are built, the type column has the correct value that ARel expected and the update passes. It is important that the #update_all be called on a scope from the parent class in your STI chain, otherwise ActiveRecord will just scope on the type again.

I would caution though, sometimes this is actually the job of a state machine, and looking at the naming of your models, I would guess the state_machine gem might be a better call than using STI and bending ActiveRecord.

Upvotes: 1

Related Questions