Economy of Effort

Twitter LinkedIn GitHub Mail RSS

ActiveRecord Save Not Updating Hstore Fields in Rails 4.0-4.1

Here’s some behavior you might not expect when using Postgres’s hstore with ActiveRecord.

ActiveRecord::Base#update_attributes does what you’d think:

[0] pry(main)> thing =
[1] pry(main)> thing.update_attributes({data: {'mykey' => 'myval'}})
[2] pry(main)> thing.reload
[3] pry(main)>['mykey']
=> "myval"

However, ActiveRecord::Base#save might not:

[0] pry(main)> thing =
[1] pry(main)>['mykey'] = 'myval'
=> "myval"
[2] pry(main)>
(0.3ms) BEGIN
SQL (0.4ms) INSERT INTO "things" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2014-12-17 04:02:03.119354"], ["updated_at", "2014-12-17 04:02:03.119354"]]
(0.9ms) COMMIT
=> true
[3] pry(main)> thing.reload
[4] pry(main)>['mykey']
=> nil

Huh? I left the SQL log line in the output here so we can see that our hstore field is indeed left out of the SQL INSERT statement entirely, which explains why the field is nil once we re-fetch the object from the database.

But why is it doing this? It turns out that, in Rails 4.0 and 4.1, this operation doesn’t mark the field as “dirty” in ActiveRecord, so the change is not detected and included in the save operation.

We can mark it manually with ActiveModel::Dirty’s attr_name_will_change!, eg.

[0] pry(main)> thing =
[1] pry(main)> thing.data_will_change!
[2] pry(main)>['mykey'] = 'myval'
=> "myval"
[3] pry(main)>
(0.3ms) BEGIN
SQL (0.3ms) INSERT INTO "things" ("created_at", "data", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2014-12-17 04:08:32.066027"], ["data", "\"mykey\"=>\"myval\""], ["updated_at", "2014-12-17 04:08:32.066027"]]
(1.0ms) COMMIT
=> true
[4] pry(main)> thing.reload
[5] pry(main)>['mykey']
=> "myval"

The documentation states that attr_name_will_change! should be called before changes to the attribute, as seen in the example above.

Doesn’t this seem like a pain? Well, as discussed in Rails issue #6127, it was expected behavior for Rails 4.0 and 4.1, but improvements to serialized attributes have been merged into Rails and will appear in Rails 4.2. Setting values and calling save will just work.

So, in the meantime, the workaround is manually marking properties as dirty with attr_data_will_change! before making and saving changes to those fields.