Skip to content
Andrew Geweke edited this page Jan 24, 2014 · 3 revisions

flex_columns doesn't really support the ActiveModel::Dirty features of ActiveRecord. Instead, a flex column will be treated as dirty the moment you read from it (access one of its fields), whether you've actually changed it or not. (On the other hand, if you neither read from nor write to it, it will remain 'clean' — i.e., not dirty.)

This may seem frustrating or wrong. However, it's for a good reason. The problem is that flex columns are very frequently used to store objects that can be (and will be) mutated, rather than assigning directly:

my_user.color_preferences = { :favorite => 'red', :least_favorite => 'green' }
my_user.save!

# ...later...
my_user = User.find(params[:id])
my_user.color_preferences[:favorite] = 'yellow'
my_user.dirty?

Here's the problem: how do we detect the modification in the second set of code, so that we can respond to the call to dirty? properly? Nobody has called #color_preferences= on the flex column; all they've done is read the Hash that's stored there — and mutated it. There's no place any method is being called (except on Hash) that could ever know that something is being changed.

The obvious way to fix this is to save off a copy of each field when we deserialize a column, then compare them using a deep equality (#== should work just fine) to determine if they've changed. However, this adds very significant overhead to each and every single use of a flex-column object, whether or not you rely on or care about this kind of tracking — we would have to #dup every flex column field every single time we deserialized, and, if you have large objects in there, that can get extremely expensive.

Since almost every object in Ruby is mutable — even Strings — there aren't really any easy wins here. Numbers are the only commonplace object that aren't, and it's not going to be a common use case that someone uses a flex_column with fields that each simply store one single number. (Storing an array or a hash of numbers is much more common, but then you're talking about Arrays and Hashes, which are back to being mutable.)

Another option would be to #freeze all of the fields on a flex column, thus requiring clients to reassign them with a new object if they wanted to change them at all. That, however, presents an API that most users would hate — I don't want to say user.prefs_map = user.prefs_map.merge(:foo => bar); I want to just say user.prefs_map[:foo] = bar.

Instead, once we deserialize a field, we just assume that it has changed. While this may end up causing the client to do extra work at times, it's much higher-performance than doing the tracking every time.

(There is definitely room to add code that would make this configurable, on a per-flex-column or even per-field basis. As always, patches are welcome; as of this writing, it seems likely that it might just not be an issue big enough to worry about.)

Clone this wiki locally