16 Apr 2013, 14:05

More Data Mapper

Share

One of the basic functions of Data Mapper is to remember which attributes in the model have been modified so that it’s easy to determine what (if anything) needs to be updated in the database. Data Mapper checks automatically on a call to #save and only writes what needs to be written. It also provides methods #dirty? and #attribute_dirty?, which tell you whether or not a record or a particular attribute has changed.

Unfortunately, while it’s easy to find out whether or not an attribute has changed, there is no easy way to see what the old value was. It’s obviously keeping the old value somewhere. We know this because when you change an attribute back to the old value, it recognizes that you’ve done so and considers it unchanged.

There is a method called #dirty_attributes, which returns a hash of changed attributes, but the keys to this hash are hashes themselves and in a format that’s used only internally in Data Mapper, making it a needlessly inconvenient method to use. Also, I’d like to avoid #dirty_attributes as it’s not part of the public API.

There is a possible workaround, suggested by some. Override the setter for the attribute and save the old value for later use.

def thing=(newthing)
  @oldthing ||= @thing
  @thing = newthing
end

I’m not going to link to the people who suggested this, though, because it’s a terrible suggestion. Since we’ve overridden Data Mapper’s setter for attribute thing, Data Mapper no longer knows that we’ve changed its value.

1.9.3-p392 :001 > record = Record.get(1)
 => ...
1.9.3-p392 :002 > record.thing
 => "teamaker"
1.9.3-p392 :003 > record.thing = "coffeemaker"
 => "coffeemaker"
1.9.3-p392 :004 > record.attribute_dirty?(:thing)
 => false

Imagine the insidious bugs that could creep in here.

1.9.3-p392 :005 > record.save
 => true
1.9.3-p392 :006 > record = Record.get(1)
 => ...
1.9.3-p392 :003 > record.thing
 => "teamaker"

Simply put, our changes are silently ignored because we’ve stupidly disabled what is arguably Data Mapper’s most important function.

Fortunately, there is a correct way to do this. Instead of setting the attributes directly, we set them using Data Mapper’s #attribute_set.

def thing=(newthing)
  @oldthing ||= @thing
  attribute_set(:thing, newthing)
end

Method #attribute_set keeps track of the changes. It’s what thing= pointed to before we overrode it.