Saving Calculated Fields in Ruby on Rails 5

In Ruby on Rails, it’s easy to build custom functions to calculate something and then display the result in your views. While this simplicity is nice, it doesn’t come without its drawbacks.

Recently, when working on a simple app, I came across a situation where loading a page was taking 0.5 seconds. This may not sound like a lot (and wouldn’t be for most sites), but in an app as simple as mine, it’s a sign that something is taking way longer than it should. Luckily, it wasn’t too difficult to determine what it was.

The Problem

Let’s start with an example: say you’re building an application that will contain purchases from a grocery store. You probably want to link the items sold in a purchase with the record from that purchase, right? Well, somewhere you’re going to have to calculate the total. Of course, I’m assuming that you don’t want the customer to calculate the total.

You could calculate the total every time that you need to load the record of the purchase, but first let’s walk through what would be happening when you calculated the total. If there are, say, 30 items in that purchase, you’ll need to load every single one of those items so that you can grab the price (we’re assuming prices don’t change for this example) and add them all together.

As you might imagine, this isn’t a very efficient way to go about things. We’d rather offload some of that computation (that would be happening an awful lot) to the disk, instead. After all, it’s generally easier to store a few bytes than spend valuable CPU time recalculating it every time you need it.

In my case, that’s exactly the sort of thing that was happening. I was working to calculate a field that wouldn’t change often but that involved loading lots of links to other records. On top of that, it was going to be loaded pretty often. It’s much more efficient for me to just store that value than to calculate it for every request.

The Solution

You’ll need to add a new field to your database, which means you’ll need to add a database migration, something like this:

rails generate migration AddTotalToReceipts total:float

After you run your migration (rails db migrate), you’ll have your new field. Now, if you generated all your scaffolding, that’d be showing up in your user interface. That’s not what you want to do, though, since we’re trying to make this easier for your users and calculate it on their behalf.

Thus, we’re going to add something like the following to our model:

before_save :calculate_receipt_total

def calculate_receipt_total
  sum_value = x + y # Whatever you need to do here to calculate
  self.total = sum_value
end

Now that method will run automatically before the record is saved, and place our calculated value into the total value, which means it’ll end up there in the database, as well.

Like I said, just how much benefit (if any) you’ll get out of this depends on your exact circumstances, but in my case it reduced a 500 ms page load to around 100 ms, which is clearly a substantial improvement.

If you’ve got any questions, drop them in the comments, and I’ll do my best to answer them!

Comments

6 responses to “Saving Calculated Fields in Ruby on Rails 5”

  1. Alexey Avatar
    Alexey

    Please, stop using float/decimal for price or quantity! Use decimal!

    1. russt Avatar
      russt

      Oh, that’s cool. I didn’t even know that decimal even existed! Thanks!

  2. Ulysses Avatar

    Oh my goodness! Impressive article dude! Thank you, However I am going through
    issues with your RSS. I don’t know the reason why I can’t join it.
    Is there anyone else having the same RSS problems? Anybody who knows the solution can you kindly respond?
    Thanks!!

    1. Greg Avatar
      Greg

      What’s a good way to add the calculated value to the new field for the already entered data using Postgres?

      Run an SQL or can it be added to the original migration?

  3. Ben Avatar
    Ben

    Hi,

    How can we use a calculated field in another calculated field?

    For instance:

    before_save :calculate_receipt_total, :another_calcul

    def calculate_receipt_total
    sum_value = x + y # Whatever you need to do here to calculate
    self.total = sum_value
    end

    def another_calcul
    self.other_column = total
    end

    The issue is that total or self.total are not yet saved and are nil.

    Any help will be appreciated

    1. João Victor Ramires Avatar

      Call a method inside other, store the return in a variable and then use it in new calculations.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.