This post is part of my Stepping Up Rails: Go From Good to Great series. Get the complete series on Teachable!
This little puzzle comes in an oft-confused part of Rails & Ruby. The attribute getter (attr_getter) that is made available to all Rails ActiveRecord objects automatically.
Remember that a Ruby object has
ApplicationRecord as its base object (or
ActiveRecord::Base before Rails 5), and
ApplicationRecord makes certain things available to the object-relational mapping (ORM), which is essentially what ActiveRecord is. This ORM is what marries the Ruby object to the database object. Think of ORM like a two-way tunnel— the Ruby object needs data so it pulls it from the database, the database takes data that is updated from the Ruby object.
But POROs (Plain Old Ruby Objects) don’t actually have a database behind them. Rails objects do. That’s where the
ApplicationRecord magic comes in.
The Rails Magic Loading Getter
One quirk to remember for Rails developers is that inside of Ruby ApplicationRecord object, there’s a secret (and magic) getter for any attribute in your database. Let’s say in a Person class you might have
class Person < ApplicationRecord
first_name + " " + last_name
This works because Rails looks to the Ruby object first for a
last_name. Assuming you haven’t overridden them, Rails doesn’t find them on your Ruby object and falls through to
method_missing (that’s the secret method that is called when a Ruby object doesn’t have a method object— it’s how so much Ruby magic works.)
The method_missing implementation looks for these fields on the Rails class which now comes from the ORM (or the database). So basically it’s like having an
attr_reader — or a ‘getter’ — automatically for any of your database fields.
But keep in mind this doesn’t work in reverse! There is no setter field that allows you to do
first_name = "John"
That’s why when you’re setting a Rails object, you must always use the
self keyword followed by dot (
self.first_name = “John”
Getter Not Setter For the Hoist
Now, you’ve told Rails to update the ORM-backed object. Why does this distinction matter? Well, first of all
first_name = "John" doesn’t actually do what you intended it to do (you probably intended it to update the object’s first name). What it does is it creates a local variable in the local scope with the name
first_name, which, confusingly, has no effect on the Rails object’s field
Consider this method on an object that checks if someone can make a request. The API rate limits their requests to 10 requests in a rolling, 10-second timeframe.
def can_make_request? unless last_request_at.nil? secs_since_last_request = (Time.now - last_request_at).floor requests_available += secs_since_last_request requests_available = [10, self.requests_available].min end
The intention of this code is to update the requests available to either 10 (if the last request was more than 10 seconds ago), or to add the number of seconds since the last request to the current count, but never allow the count to exceed 10. Unfortunately, we aren’t actually saving the data back to the database.
byebug into this method reveals a curious thing:
def can_make_request? unless last_request_at.nil? secs_since_last_request = (Time.now - last_request_at).floor byebug requests_available += secs_since_last_request requests_available = [10, requests_available].min end
Here, if we ask for either
name, we get it. But if we ask for
requests_available, Rails tells us the result is
Notice that we haven’t even modified requests_available, and unlike
name, this field, even when read (with the getter) comes up as nil:
4: 5: def can_make_request? 6: unless last_request_at.nil? 7: secs_since_last_request = (Time.now - last_request_at).floor 8: byebug => 9: requests_available += secs_since_last_request 10: requests_available = [10, self.requests_available].min 11: end 12: end 13: (byebug) id 1 (byebug) name "Cruickshank Inc" (byebug) requests_available nil
How does that make sense?
Well, it’s the Ruby hoist we were talking about before, and it demonstrates why Rails provides only a getter and not a setter. You see, inside of the scope of
can_make_request? Ruby has already seen on line 8 that we are going to use a local variable called
requests_available. When we call it on line 8, we actually get
Here, the local variable — which hasn’t even been set yet— is returned to us instead of the Rails getter that calls through to the database. (which you can see when we call
This is easily fixed! Just remember to use
self. whenever we want to save information back to the object. (You will also need to actually save the object too!)
The basic rule of thumb is:
You can get a Rails database field using the magic getter that is its own name, but if you want to save back to the database, use self.____ = instead.