57

So I know how to override the default getters for attributes of an ActiveRecord object using

def custom_getter
  return self[:custom_getter] || some_default_value
end

I'm trying to achieve the same thing however for a belongs to association. For instance.

class Foo < AR
  belongs_to :bar

  def bar
    return self[:bar] || Bar.last
  end
end

class Bar < AR
  has_one :foo
end

When I say:

f = Foo.last

I'd like to have the method f.bar return the last Bar, rather than nil if that association doesn't exist yet.

This doesn't work however. The reason is that self[:bar] is always undefined. It's actually self[:bar_id].

I can do something naive like:

def bar
  if self[:bar_id]
    return Bar.find(self[:bar_id])
  else
    return Bar.last
  end
end

However this will always make a db call, even if Bar has already been fetched, which is certainly not ideal.

Does anyone have an insight as to how I might have a relationship such that the belongs_to attribute is only loaded once and has a default value if not set.

brad
  • 31,987
  • 28
  • 102
  • 155

4 Answers4

78

alias_method is your friend here.

alias_method :original_bar, :bar
def bar
  self.original_bar || Bar.last
end

The way this works is that you alias the default "bar" method as "original bar" and then implement your own version of "bar". If the call to original_bar returns nil then you return the last Bar instance instead.

Ian Vaughan
  • 20,211
  • 13
  • 59
  • 79
Randy Simon
  • 3,324
  • 21
  • 19
39

i found that using "super" is the best way

def bar
  super || Bar.last
end

I hope this helps you :D

mech
  • 2,775
  • 5
  • 30
  • 38
alxibra
  • 727
  • 1
  • 7
  • 12
  • 3
    This is such a great answer, as it just uses plain old Ruby inheritance. And it's a lot simpler than the accepted answer. – Craig Buchek Apr 05 '19 at 16:43
10

Randy's answer is spot on, but there's an easier way to write it, using alias_method_chain:


def bar_with_default_find
  self.bar_without_default_find || Bar.last
end
alias_method_chain :bar, :default_find

That creates two methods - bar_with_default_find and bar_without_default_find and aliases bar to the with method. That way you can explicitly call one or the other, or just leave the defaults as is.

Ian Vaughan
  • 20,211
  • 13
  • 59
  • 79
Cory Foy
  • 7,202
  • 4
  • 31
  • 34
  • Cool solution because you can use `_without_default_find` to have a notion of the original assocation method. Was the only one I was able to get working. Thanks – grant Jun 02 '16 at 18:15
  • I believe `alias_method_chain` is deprecated now. – Nick Mar 03 '20 at 14:14
0

Building upon other answers here, you may also want to handle assignment operations as well.

Alias method:

alias_method :original_bar, :bar
alias_method :original_bar=, :bar=
def bar
  self.original_bar ||= Bar.last
end

Super method:

def bar
  super || bar = Bar.last
end

Where this was useful for me was when using Bar.find_or_initialize_by which meant the record wasn't always persisted, and also any non-persisted changes would reflect on the parent record as well.

Lulzagna
  • 139
  • 1
  • 3