Reputation: 3
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
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