Question

I'm wondering if there is something at work here that I don't understand or if I've run into a bug in ActiveRecord (4.1.1).

I have a database full of records with only one attribute, a field a bit of JSON in it. I take one and try to update it like so.

test = Submission.find(1)
test.update_attribute('json_data',similar_but_different_json(test.json_data))

Let's assume the method similar_but_different_json makes a small update to that JSON. In my case I'm fixing some data errors that were created by a broken form.

When doing this, I don't get any errors, I show a commit in the console but no data submitted and get a return of true.

In order to actually update the record I have to do this.

test = Submission.find(1)
old_json_data = test.json_data
test.json_data = ""
test.json_data = similar_but_different_json(old_json_data)
test.save

What seems to be happening is that ActiveRecord doesn't identify that a change has been made that has to be saved. Could this be why setting the field to an empty string then back to JSON allows the record to save?

Was it helpful?

Solution

will_change!

You can also use:

test.json_data_will_change!   # Goes before the save.

This will tell ActiveModel that the attribute, json_data, has changed (i.e. it's dirty ← there's a joke there somewhere) and will update the value properly on save.

See Rails is not saving an attribute that is changed for some more details as well.

OTHER TIPS

I don't understand why exactly the object is not marked dirty. A workaround is to use update_columns:

test.update_columns(json_data: similar_but_different_json(test.json_data))

It will execute an UPDATE query directly in the DB, without any validation, dirty check, etc... The json_data field must not be read-only though.

ActiveModel (by 4.1.1) doesn't have a way to track "inline" modifications on attributes.

Your 'similar_but_different_json' method is probably making inline modifications on the string.

Just duplicate the string before modifying it.

test = Submission.find(1)
test_json_data_duplicate = test.json_data.dup 
test.update_attribute('json_data',similar_but_different_json(test_json_data_duplicate))

When you did ...

test.json_data = ""

... ActiveModel could catch the change because you are setting it to a new String object that happens to be empty. So when you call update_attribute the model has already known that the attribute has changed.

If you try to empty the string in an inline manner your trick will not work.

test = Submission.find(1)
old_json_data = test.json_data
test.json_data.clear # Instead of test.json_data = ""
test.json_data = similar_but_different_json(old_json_data)
test.save

ActiveModel::Dirty

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top