KEEP IN TOUCH

Join Newsletter

CALL NOW

Share with:


Recent Posts





Identity Crisis

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.