in rails ruby graphql ~ read.

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 as creator
  • 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.