-
Notifications
You must be signed in to change notification settings - Fork 9
Change Detection
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 answer
Not at all obvious: originally, we had a method called #touched? that let you know whether the given object had been changed at all. It simply got set on +#[]=+, above. The problem with this is that very frequently, +flex_columns+ is used to store complex data structures (because that's one of the things that's dramatically easier in a serialized JSON blob than in a traditional relational structure). But if you have an array stored, and you call #<< on it to append an element, then +#[]=+ never gets called at all -- because it's still the same object, just with different contents.
We could have worked around this by saving off a copy of each field when we deserialized, then comparing 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.)