Freeze Ray

"With my freeze ray I will stop the pain." --Dr. Horrible

The problem

ActiveRecord's dirty tracking feature is broken. Awesome, but broken. Let me demonstrate.

>> p = Post.last
=> #<Post id: 261, ... >
>> p.title
=> "A quick update"
>> p.changes
=> {}
>> p.title << " about my pet frogs"
=> "A quick update about my pet frogs"
>> p.changes
=> {}

Hang on, now! We changed the title! Why didn't we see this?

=> {"title"=>["A quick update", "A quick update about my pet frogs"]}

Because ActiveRecord doesn't have any way of knowing that we changed the attribute. That's because we mutated it in place. Rather than replace the string with a new one, we changed the one it already had. That doesn't involve calling #title= on the Post, so the Post doesn't realize that the actual value changed.

ActiveRecord provides one solution: call #title_will_change! first. That tells the Post to remember the current value of the title to compare to the old one. Observe:

>> p.title << " whom I love dearly"
=> "A quick update about my pet frogs whom I love dearly"
>> p.changes
=> {"title"=>["A quick update about my pet frogs", "A quick update about my pet frogs whom I love dearly"]}

Well, sure, that works. But you have to be careful. What if you forget to that at some point?

Another Solution

Freeze Ray offers another solution. Just mark the attributes you want to track as attr_frozen:

class Post < ActiveRecord::Base
  attr_frozen :title
end

Now try to mess up dirty tracking:

>> p = Post.last
=> #<Post id: 261, ... >
>> p.title
=> "A quick update"
>> p.title << " about my pet frogs"
TypeError: can't modify frozen string
        from (irb):36:in `<<'
        from (irb):36
>> p.title
=> "A quick update"
>> p.title.frozen?
=> true

You can't do it!

Obviously, this isn't very useful in a case where you need to change attribute objects in place. On the other hand, I can't think of a reason you'd want to change them in place. Now, if you try, you'll know.