I’m looking for a way to extend one of my models to allow some level
of abstraction between what goes into it and how it is stored.
For example, say I have a Product and I want to set it’s price. A
person using the website will type the price in euros. Internally, I’d
like to store the price as an integer value of cents.
I can currently do this with some ugly code in the controller, but I’d
like the model to just accept a price in euros and automatically
convert it to cents.
In plain Ruby, I could do something like this:
Take in prices in euros, store them as cents internally (doesn’t
work right )
def price
unless cents.nil?
return cents / 100.0
else
return cents
end
end
def price=(euros)
cents = euros * 100
end
But it doesn’t work in RoR. Is there something I’m missing? I have a
horrible feeling there should be a ‘self’ somewhere in there…
have a look at the before_save callback [1], which lets you do do
stuff like euro/cent conversion before a record is saved.
If you want conversion to happen before the record is saved, I would
use a specialized setter method. Supposing your column is called
‘cents’ and you want to be able to set the value of that column by
passing in Euros, do something like this in your model:
I’m looking for a way to extend one of my models to allow some level
of abstraction between what goes into it and how it is stored.
But it doesn’t work in RoR. Is there something I’m missing? I have a
horrible feeling there should be a ‘self’ somewhere in there…
You’re talking about Facade Columns, where the data stored in the
database is in a different format to how you handle it in the
application. When you overwrite accessor methods, you need to use the
read_attribute and write_attribute methods to get the data from/put the
data to the database.
Your price example should be as simple as:
def price
read_attribute(‘price’) / 100.0
end
def price=(euros)
write_attribute(‘price’, euros * 100)
end
(non-working) code. What I find confusing, is that for READING, just
using ‘cents’ works. (the ‘price’ method worked, but ‘price=’ did
not).
Is this an inconsistency in AR, or Ruby, or is there something I’m missing?
Internally AR keeps your attribute values in a Hash. When you write
self[:cents] = 10
you’re writing directly to the attributes Hash. If instead you write
cents = 10
you’re initializing a local variables with value 10.
As for reading attributes, AR gives you a shortcut. When you just use
“cents” inside your model instance, and there is no variable or method
‘cents’ in your current scope, then ActiveRecord::Base will intercept
your call, and figure out that you’re trying to call a method that has
the same name as one of the attributes. AR will then be nice enough
to return you the attribute in question.
Internally AR keeps your attribute values in a Hash. When you write
self[:cents] = 10
you’re writing directly to the attributes Hash. If instead you write
cents = 10
you’re initializing a local variables with value 10.
What I tried to suggest with my too-cute post (sorry, Christmasy mood)
was that David write
self.cents = ...
which is the same as using self[:cents].
That’s one of the major traps of Ruby: calls to setter methods
inside its class require a self receiver, otherwise the variable
is interpreted as a local. I don’t fully understand why it has
to be this way.
–
We develop, watch us RoR, in numbers too big to ignore.
That’s one of the major traps of Ruby: calls to setter methods
inside its class require a self receiver, otherwise the variable
is interpreted as a local. I don’t fully understand why it has
to be this way.
I didn’t fully understand that myself. Looking about for the answer I
found [1]; here’s the relevant portion:
"Sidebar: Using Accessors Within a Class
Why did we write self.leftChannel in the example on page 74? Well,
there’s a hidden gotcha with writable attributes. Normally, methods
within a class can invoke other methods in the same class and its
superclasses in functional form (that is, with an implicit receiver of
self). However, this doesn’t work with attribute writers. Ruby sees
the assignment and decides that the name on the left must be a local
variable, not a method call to an attribute writer."
Thanks guys, both of you; this is fantastic. No more messy code in the
controller!
Gerret, in your example code, you access cents through self[:cents].
That seems to be the major difference between your code and my
(non-working) code. What I find confusing, is that for READING, just
using ‘cents’ works. (the ‘price’ method worked, but ‘price=’ did
not).
Is this an inconsistency in AR, or Ruby, or is there something I’m
missing?