Hao's Thoughts

Ruby, Rails, Objective-C and everything else

Gotcha: Rails Doesn't Track in Place Changes to Serialized Column

Believe it or not, it’s been 6 years since the ticket was created about “Rails doesn’t track in place changes to serialized column”, it’s really a tricky problem rare people will run into. Consider the following scenario:

1
2
3
class Answer < ActiveRecord::Base
  serialize :options, Array
end
1
2
3
4
5
6
7
8
9
10
11
12
13
>> a = Answers.new(options: [1, 2, 3])

>> a.options
=> [1, 2, 3]

>> a.changed?
=> false

>> a.options << 4
=> [1, 2, 3, 4]

>> a.changed?
=> false

As commented by Jose Valim link, it’s a feature of Rails and it’s documented somewhere. The workaround is issue another options_will_change! to mark the object changed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>> a = Answers.new(options: [1, 2, 3])

>> a.options
=> [1, 2, 3]

>> a.changed?
=> false

>> a.options << 4
=> [1, 2, 3, 4]

>> a.options_will_change!

>> a.changed?
=> true

It gets more tricky especially when you are using accepts_nested_attributes_for, the Rails will ditch the changes to the children objects if changed? returns false.

1
2
3
4
class Question < ActiveRecord::Base
  has_many :answers, dependent: :destroy
  accepts_nested_attributes_for :answers
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# spec/controllers/questions_controller_spec.rb

describe QuestionsController do
  describe "PUT /update" do
    it "updates the question with its answers" do
      q1 = Question.create
      a1 = Answer.create(question: q1, options: [1, 2, 3])

      put :update, id: q1.id, question: {
        answers_attributes: {
          "0" => {id: a1.id, options: [1, 2, 3, 4]}
        }
      }

      a1.reload
      expect(a1.options).to eq [1, 2, 3, 4] # FAIL!!
    end
  end
end

It really got me and hope this will help somebody like me, happy hacking!!

Comments