GraphQL Performance Testing (Part 1): Set up a Rails API
I have recently been looking into django and other frameworks. I have been working quite a bit with the rails GraphQL gem. In this article, I want to test the difference between both frameworks in providing graphql data to a common interface
In this sample project we'll start an app for tracking invoices.
You can also download a completed version of the code in this article from github
Step 1: Design
In order to get a good test of each system, we want something that will provide a few joins and multiple record lookups. We will add 3 models, user, invoice and invoice items.
Invoice
- will have a
date
(required) - will have a
number
(required) - will
have_many
invoice items - will
have_one
user ascreator
- will have an amount that is the total of all of the invoice items
Invoice Items
- will have an amount, description and a price
- total for the item will be the
price * amount
- will
belong_to
an invoice
User
- will have
first_name
,last_name
,email
- will
have_many
invoices
Step 2: Setup
For this setup we'll create a new rails site in api mode.
Since this is a performance test, we can use both django and rails as GraphQL api's and not to serve pages.
1. Create the initial Rails API site
First a stripped down version of rails for the api site
rails new --api rails_api --skip-bundle --skip-sprockets --skip-action-mail{er,box} --skip-action-text -skip-active-storage --skip-test --javascript=\"react\" --database=\"postgresql\"
Next a few more gems we'll need...
# Gemfile
# To help deal with our currency
gem \"money-rails\", \"~>1\"
# for graphql
gem \"graphql\", \"~>1.9.1\"
gem \"graphiql-rails\"
gem \"search_object_graphql\"
# in development section
group :development, :test do
gem \"better_errors\", \"~> 2.5\"
gem \"binding_of_caller\"
gem \"annotate\"
gem \"awesome_print\"
gem \"rubocop\"
gem \"faker\", \"~> 2.14.0\"
end
Finally, in the rails_api
folder run:
$ bundle install
and create an initializer file for the money gem
# config/initializers/money.rb
# Get rid of Deprecation warnings
Money.locale_backend = :currency
MoneyRails.configure do |config|
config.default_currency = :usd
end
2. Create the tables
Now that we have the pg gem installed, we can create a database and make our initial tables.
First, create the database:
$ rails db:create
Next, Users:
$ rails g model users first_name:string last_name:string email:string
$ rails db:migrate
In a normal app, we might use the Devise gem to manage user logins and add validation for the user's email address. As this is a perfomance step, we can skip that for now.
Third, Invoices:
$ rails g model invoices date:date number:string creator:references
Creating a reference to the users table as creator will require one more change to the migration
Update the references method with modifications to table_name
and foreign_key
t.references :creator, null: false, table_name: :users, foreign_key: {to_table: :users}
and as always ...
$ rails db:migrate
Note: referencing the users table in this way is probably not totally necessary for this test. But in a real-word app, an invoice table might reference multiple users. Naming the relationship also gives us an idea of what relationship the user has to the invoice in the database layer.
Finally, create the Invoice Items:
$ rails g model invoice_items amount:integer description:string price:monetize invoice:references
$ rails db:migrate
We can also use the annotate gem to automatically add the current database schema as a comment in our models
$ bundle exec annotate --models
3. Create Relationships
Next we will add the relationship information into rails
in app/models/user.rb
add
has_many :invoices
in app/models/invoice.rb
belongs_to :creator, class_name: \"User\"
has_many :invoice_items, dependent: :destroy
in app/models/invoice_items.rb
belongs_to :creator, class_name: \"User\"
has_many :invoice_items
4. Create sample data
At this point, we are ready to generate some sample data using the Faker gem.
We could create a rake task or just input commands into the rails console. In this case, since the point of the app is to serve data, we will create a database migration with sample data. (Even though we are not creating or updating a table, we can still run code and ensure the data is available after migrating the database)
$ rails g migration create_sample_data
update the migration:
# db/migrate/[timestamp]_create_sample_data.rb
class CreateSampleData < ActiveRecord::Migration[6.0]
def up
50.times do
User.create first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
email: Faker::Internet.email
end
250.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
(5..25).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
end
def down
Invoice.all.map(&:destroy)
User.all.map(&:destroy)
end
end
then run it
$ rails db:migrate
you should now be able to run rails console and test for generated data like the following:
User.first
=> #<User id: 251, first_name: \"Bree\", last_name: \"Pacocha\", email: \"michal.larson@bosco.name\", created_at: \"2021-01-11 20:06:23\", updated_at: \"2021-01-11 20:06:23\">
Invoice.last
# => <Invoice id: 502, date: \"2021-12-19\", number: \"14866420-W\", creator_id: 289, created_at: \"2021-01-11 20:06:23\", updated_at: \"2021-01-11 20:06:23\">
Invoice.last.invoice_items
=> #<ActiveRecord::Associations::CollectionProxy [#<InvoiceItem id: 7434, amount: 128, description: \"Cray craft beer normcore occupy plaid 90's.\", price_cents: 50650, price_currency: \"USD\", invoice_id: 502, created_at: \"2021-01-11 20:06:23\", updated_at: \"2021-01-11 20:06:23\">, ...]
Now that we have the initial models in or API set up we are ready for the next step.
In the next article we will finish up the spec for our models by adding methods to calculate totals for each invoice item and the total for each invoice. We will also get the graphql schema up and running.