Sean Magyar
Sean Magyar

Reputation: 2380

rails when to set instance variable with ||=

I was reading an article and ran into the 1st example code. In the model the instance variable is set to avoid unnecessary queries. I also saw this in one of the railscasts (2nd example). On the other hand I read in more articles that if I use this pattern then my app may won't be thread safe so I can't take advantages of my puma webserver.

Could sby tell me when/where I should use this pattern?

1st example:

def first_order_date
  if first_order
    first_order.created_at.to_date
  else
    NullDate.new 'orders'
  end
end

private

def first_order
  @first_order ||= orders.ascend_by_created_at.first
end

2nd example

def shipping_price
  if total_weight == 0
    0.00
  elsif total_weight <= 3
    8.00
  elsif total_weight <= 5
    10.00
  else
    12.00
  end
end

def total_weight
  @total_weight ||= line_items.to_a.sum(&:weight)
end

UPDATED questions

1st example

As I see this 'first_order_date' is always called on an object (https://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object), so I don't exactly see how the extra query can be avoided. I'm sure I'm wrong but according to my knowledge it could be just

def first_order_date
  if orders.ascend_by_created_at.first
    first_order.created_at.to_date
  else
    NullDate.new 'orders'
  end
end

Or the may use the @first_order somewhere else as well?

2nd example

The code in the original question is not equivalent with this?

def shipping_price
  total_weight = line_items.to_a.sum(&:weight)
  if total_weight == 0
    0.00
  elsif total_weight <= 3
    8.00
  elsif total_weight <= 5
    10.00
  else
    12.00
  end
end

I see here what they achieve with defining total_weight, but why is it better to use an instance variable over my example?

Upvotes: 0

Views: 1392

Answers (2)

joshua.paling
joshua.paling

Reputation: 13952

The short story is: your code should be fine on Puma.

With regards to thread safety in the context of Puma, what you have to worry about is changing stuff that might be shared across threads (this usually means stuff at a class level, rather than at an instance level - I don't think Puma will be sharing instances of objects across it's threads) - and you're not doing that.

The ||= technique you refer to is called 'memoization'. You should read the full article at https://bearmetal.eu/theden/how-do-i-know-whether-my-rails-app-is-thread-safe-or-not/, in particular the section on memoization.

To answer the questions in your comments:

  1. Why is not enough to define total_weight = line_items.to_a.sum(&:weight) in the first line of shipping_price method? As I see it would run the query only once

OK, so if the shipping_price method only gets called once per instance of that class, then you're right - there's no need for memoization. However, if it gets called multiple times per instance, then every time, it has to execute line_items.to_a.sum(&:weight) to calculate the total.

So let's say you called shipping_price 3 times in a row for some reason, on the same instance. Then without memoization, it would have to execute line_items.to_a.sum(&:weight) 3 times. But with memoization, it'd have to execute line_items.to_a.sum(&:weight) only once, and the next two times it'd only have to retrieve the value of the @total_weight instance variable

  1. Where do you use 'memoization' in you rails apps?

Hmm... I'm not sure I can give a good answer to that without writing a really long answer and explaining a lot of context etc. But the short story is: whenever there's a method that fits all of the following:

  • might be called multiple times per instance
  • does something time consuming (eg. queries the database or something)
  • the result of that method can be safely cached as it's unlikely to change between the different times the method is called (on a per-instance basis)

A good analogy might be: if someone asks you the time, you check your watch (ie, the time consuming action). If they ask you the time again, 1 second later, you don't need to check your watch again - you basically say "I already just checked, it's 9:00am". (That's kinda like you're memoizing the time - saving you having to check your watch, because the result won't have changed since last time you asked).

Upvotes: 6

tadman
tadman

Reputation: 211660

In this case it's used to avoid executing code repeatedly when the answer will be the same.

Imagine this version:

def shipping_price
  if line_items.to_a.sum(&:weight) == 0
    0.00
  elsif line_items.to_a.sum(&:weight) <= 3
    8.00
  elsif line_items.to_a.sum(&:weight) <= 5
    10.00
  else
    12.00
  end
end

That's a lot of heavy lifting for one simple thing, isn't it? The ||= pattern serves to cache the results.

Upvotes: 2

Related Questions