The WegoWise Development Blog

Rails Gotcha: Saving STI Records

| Comments

Let’s say you have a persisted instance of a model, RedCar, that uses STI and is descended from Car. You have an action that changes the type of the record to BlueCar. You use #becomes to cast the record to the new type before the save because you want to use the validations and callbacks from the new model:

1
2
3
4
5
car = Car.find(1)
car.type # => 'RedCar'
car = car.becomes(BlueCar)
car.type # => 'BlueCar'
car.save!

But wait! If you retrieve car from the database again you’ll see that it’s still an instance of RedCar. In fact, you’ll see that no changes are saved to the database:

1
2
3
4
5
6
7
8
car = Car.find(1)
car.brand # => 'GM'
car = car.becomes(BlueCar)
car.brand = 'Ford'
car.save!
car = Car.find(1)
car.type # => 'RedCar'
car.brand # => 'GM'

Doing an explicit update_column on type here doesn’t help:

1
2
3
4
5
car = Car.find(1)
car = car.becomes(BlueCar)
car.update_column(:type, 'BlueCar')
car = Car.find(1)
car.type # => 'RedCar'

Also, note: no exceptions are raised. We’ll save you any more head-scratching by pointing out the SQL that is generated:

1
2
UPDATE `cars` SET `type` = 'BlueCar', `brand` = 'Ford'
WHERE `cars`.`type` IN ('BlueCar') AND `cars`.`id` = 1

The problem is that Rails STI scopes the statement to WHERE `cars`.`type` IN ('BlueCar') even for UPDATES and even when you’re trying to change the type itself. Woops.

So what’s the solution? As far as we can tell you have to do a separate update after you’ve passed validation. We ended up implementing this in a before_filter along these lines:

1
2
3
4
5
6
7
class Car < ActiveRecord::Base
  before_update :really_change_type, :if => :type_changed?

  def really_change_type
    Car.where(id: id).update_all(type: type)
  end
end

Comments