Beginner
This post is part of my Stepping Up Rails: Go From Good to Great series. Get the complete series on Teachable!
Javascript developers talk about the hoist, where a reference to a variable inside of a given scope in a section of code below affects code above it, effectively hoisting the variable up into the reserved namespace. Did you know that Rails has its own hoist effect?
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
def name
first_name + " " + last_name
end
end
This works because Rails looks to the Ruby object first for a first_name
and 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 first_name
.
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
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.
Dropping a 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
end
Here, if we ask for either id
or name
, we get it. But if we ask for requests_available
, Rails tells us the result is nil
.
Notice that we haven’t even modified requests_available, and unlike id
and 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
Ruby Hoist
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 nil
because Ruby has already hoisted the variable’s definition and namespace to the top of the local scope, just like in Javascript.
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 id
and name
).
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.