ActiveRecord: self.field vs field

TL;DR version:
“self.field” is only necessary when you are assigning a new value to a field.

It always confused me why it seemed like sometimes using the field name from the database failed to work in model methods that I wrote. When the method was simple, things would work fine, but then something would happen and it suddenly stopped working. I never quite put together what was going on until now. I would start out with something like this:

# Very simple, overused, contrived example
def full_name
  "#{prefix} #{first_name} #{last_name}"
end

It works beautifully and looks clean. But then, I’d need to add some defaults, like so.

def full_name
  prefix = "Mr." if prefix.blank?
  first_name = "First" if first_name.blank?
  last_name = "Last" if last_name.blank?
  "#{prefix} #{first_name} #{last_name}"
end

At first glance, it looks right, but it’s not. It causes full_name to produce “Mr. First Last”, every time you run it. WTF? Here’s where I would usually overreach and get it horribly wrong. I know that the fields are available as “self.field”, so I make one more change.

def full_name
  self.prefix = "Mr." if self.prefix.blank?
  self.first_name = "First" if self.first_name.blank?
  self.last_name = "Last" if self.last_name.blank?
  "#{self.prefix} #{self.first_name} #{self.last_name}"
end

So, one WTF leads right to another. This code is horrible. It’s ugly and it feels like there should be a better answer. In this example, it’s not so bad, but what usually ends up happening is this type of code gets written in a method that is already fairly ugly, and because of a simple misunderstanding, it goes directly from bad to worse.

Finally having some time, and just being fed up enough, I did some searching and found this. It ends up being fairly easy to understand once you think about it. In ActiveRecord models, the fields are methods. The way Ruby works, writing “field = ” creates a local variable instead of calling the “field=” method on self. It is the very act of assigning the field with a default that causes the issue. So, you can scale it back a little and rewrite the code like this. You only need “self.field” when assigning a new value.

def full_name
  self.prefix = "Mr." if prefix.blank?
  self.first_name = "First" if first_name.blank?
  self.last_name = "Last" if last_name.blank?
  "#{prefix} #{first_name} #{last_name}"
end

Much better.