When last we left off, we had just created a rails api site, set up the models and added some sample data generation.
You can download the github repo and checkout the
Part2 branch to begin with this article.
_(any file paths references are all relative to the rails\api folder)
Now, we need to make sure we update the Rails models so we have the ability to get totals for
Since an Invoice is made up of many items, let's start with the items first
Calculating totals for invoice items
We have the amount and price_cents fields from the database. Creating a total is as easy as multiplying the two.
# app/models/invoice_item.rb def total_cents amount * price_cents end
It's worth noting that we are using cents here to avoid any inconsistencies with floating point math. This will not matter much for performance. But, it is a valid consideration in a real-world app. It also gives us a chance to evaluate values calculated by the application layer, rather simply pulling values from the database.
Since we added the
rails-money gem during setup, we should also add a method that returns a money object for easy manipulation or currency conversion. By convention, this should have the same method name without the
# app/models/invoice_item.rb def total Money.new total_cents end
Calculating totals for invoices
Draft 1: Loop over the collection
For our first draft, we can simply use the reduce method to loop over the invoice items collection and add everything together
# app/models/invoice.rb def total_cents invoice_items.reduce(0) do |total, item| total + item.total_cents total end end def total Money.new total_cents end
For a single record, this works reasonably fast, but it is making a database request for each record. This adds ~0.1 - 0.3ms for each record. Let's see if we can do better.
# app/models/invoice.rb def total_cents_second invoice_items.sum(:price_cents) end
The difference with this method is that we are doing the summation in the database, rather than in the application layer.
Surprisingly, the results are about the same. The second draft still adds ~0.1 - 0.3ms for each record. I suspect that rails 6 has included enhancements under the hood.
Time to test the difference!
Benchmarking our first drafts
First, I added another 2,000 records in order to get a good statistical sample, using the same code for setting up the initial data. I also increased the possible number of generated invoice items from 25 to 250.
2000.times do invoice = Invoice.create date: Faker::Date.in_date_period(month: 12), number: Faker::IDNumber.spanish_citizen_number, creator: User.order(Arel.sql('RANDOM()')).first # Invoice Items (15..250).to_a.sample.times do invoice.invoice_items.create amount: (5..250).to_a.sample, description: Faker::Hipster.sentence(word_count: 3), price_cents: (25..250_000).to_a.sample end end
In the rails console I added the following Benchmark code:
require 'benchmark' Benchmark.bm do |benchmark| benchmark.report('Invoice#total_cents_second') do Invoice.all.each do |invoice| invoice.total_cents_second end end benchmark.report('Invoice#total_cents') do Invoice.all.each do |invoice| invoice.total_cents end end end
| method | real | stime | utime | total | |----------------------------|----------|----------|----------|----------| | Invoice#total_cents | 9.646610 | 0.384991 | 8.691589 | 9.076581 | | Invoice#total_cents_second | 1.147491 | 0.099628 | 0.824540 | 0.924168 |
comparing total times shows the second draft to be about 9.8 times faster
clearing the cache, quitting the console and running the tests in the opposite order resulted in a similar result
| method | real | stime | utime | total | |----------------------------|-----------|----------|----------|----------| | Invoice#total_cents | 10.072655 | 0.383990 | 9.031934 | 9.415925 | | Invoice#total_cents_second | 1.68221 | 0.143680 | 1.175744 | 1.319424 |
The total time went up for the second draft. But it's still about 7 times faster under load.
After some performance testing. It seems preferable to go with the second draft of the method.
You can check out the final version in the Part2-Final branch of this repository
In the next installment, we'll actually create the GraphQL API to take advantage of our models.