Default stegosaurses
Ever heard of Jonathan Viney’s active_record_defaults plugin? It’s fantastic. In a nutshell, it enables you to specify default values for model attributes. If an attribute isn’t set when initialising a new instance, the attribute’s set to whatever the default value is.
For example, let’s rebuild that Stegosaurus class from a previous post:
class Stegosaurus < ActiveRecord::Base attr_accessible :tail_spikes, :back_plates validates_numericality_of :tail_spikes,nly_integer => true, :greater_than_or_equal_to => 0 validates_numericality_of :back_plates,
nly_integer => true, :greater_than_or_equal_to => 0end
With that, if we created a new Stegosaurus and didn’t specify any attributes, not only would it have no tail spikes or back plates, but the model instance wouldn’t be valid.
To fix that, all we need to do is add two lines:
class Stegosaurus < ActiveRecord::Base attr_accessible :tail_spikes, :back_plates default :tail_spikes => 4 default :back_plates => 6 validates_numericality_of :tail_spikes,nly_integer => true, :greater_than_or_equal_to => 0 validates_numericality_of :back_plates,
nly_integer => true, :greater_than_or_equal_to => 0end
And voila, new Stegosaurus objects will automatically have 4 tail spikes and 6 back plates:
>> s = Stegosaurus.new=> #<Stegosaurus tail_spikes: 4, back_plates: 6>>> ?> s.tail_spikes=> 4>> s.back_plates=> 6
However, if used improperly, this can lead to several hours of pounding your head on your desk. Note that that the default values we used above, 4 and 6, are immutable objects. You can’t change them. You can perform operations on them, such as addition (4 + 1), but that never changes the object.
Say we want our Stegosaurus class to have an array specifying its diet that defaults to “fern” and “cycad”:
class Stegosaurus < ActiveRecord::Base attr_accessible :tail_spikes, :back_plates, :diet default :tail_spikes => 4 default :back_plates => 6 default :diet => %w(fern cycad) validates_numericality_of :tail_spikes,nly_integer => true, :greater_than_or_equal_to => 0 validates_numericality_of :back_plates,
nly_integer => true, :greater_than_or_equal_to => 0end
That looks right. But watch this:
=> charles = Stegosaurus.new=> #<Stegosaurus tail_spikes: 4, back_plates: 6, diet: ["fern", "cycad"]>>> ?> arnold = Stegosaurus.new=> #<Stegosaurus tail_spikes: 4, back_plates: 6, diet: ["fern", "cycad"]>>> ?> charles.diet < < 'moss'=> ["fern", "cycad", "moss"]>> ?> arnold.diet=> ["fern", "cycad", "moss"]
We didn’t mean to do that…Arnold doesn’t want to eat moss!
At the moment, when the Stegosaurus class is initialised, it creates one Array for the default “diet” attribute of all future Stegosaurus objects. In other words, Charles’ and Arnolds’ “diet” attributes were references to the same object:
>> charles.diet.object_id=> 19103090>> ?> arnold.diet.object_id=> 19103090
How do we fix that? We tell the Stegosaurus class to create a new Array for each model instance. Simply change this:
default :diet => %w(fern cycad)
to this:
default :diet => Proc.new { %w(fern cycad) }
Now each time we create a new Stegosaurus, that Proc will fire, and create a new “diet” Array:
=> charles = Stegosaurus.new=> #<Stegosaurus tail_spikes: 4, back_plates: 6, diet: ["fern", "cycad"]>>> ?> arnold = Stegosaurus.new=> #<Stegosaurus tail_spikes: 4, back_plates: 6, diet: ["fern", "cycad"]>>> ?> charles.diet < < 'moss'=> ["fern", "cycad", "moss"]>> ?> arnold.diet=> ["fern", "cycad"]
I highly recommend giving active_record_defaults a try. It’s very handy, and very easy to use.
And by the way, yes, I was bitten by this problem. Why else would I be writing about it! Can you guess how I discovered it, though? … My tests picked it up!