Pull to refresh

GraphQL without N+1 is easy. N1Loader is a must

Reading time2 min
Views1.5K

N1Loader is designed to provide a simple way for avoiding N+1 issues
of any kind. Gladly, it's super easy to integrate with your GraphQL
API. Without further delay, let's look at a simple but yet detailed
example.

# Add N1Loader with ArLazyPreload integration 
require "n1_loader/ar_lazy_preload"
require 'graphql'

# Setup SQLite tables in memory. That is irrelevant to the example.
require_relative 'context/setup_database'
# ArLazyPreload requires Rails application. This is needed to avoid that.
require_relative 'context/setup_ar_lazy'

class User < ActiveRecord::Base
  has_many :payments

  n1_optimized :payments_total do |users|
    # Fetch payments for a group of users we will preload in a single query
    total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }

    users.each do |user|
      total = total_per_user[user.id]
      # No promises here, simply add a value for to a user.
      fulfill(user, total)
    end
  end
end

class Payment < ActiveRecord::Base
  belongs_to :user

  validates :amount, presence: true
end

# Enable ArLazyPreload globally
ArLazyPreload.config.auto_preload = true
# Or use +preload_associations_lazily+ when loading objects from database

class UserType < GraphQL::Schema::Object
  field :payments_total, Integer
end

class QueryType < GraphQL::Schema::Object
  field  :users, [UserType]

  def users
    User.all
  end
end

class Schema < GraphQL::Schema
  query QueryType
end

query_string = <<~GQL
  {
    users {
      paymentsTotal
    }
  }
GQL

# No N+1. And never will be!
p Schema.execute(query_string)['data']

N1Loader supports arguments that you can pass through GraphQL API. There will no N+1 still.

class User < ActiveRecord::Base
  n1_optimized :payments_total do
    argument :from
    argument :to

    def perform(users)
      total_per_user =
        Payment
          .group(:user_id)
          .where(created_at: from..to)
          .where(user: users)
          .sum(:amount)
          .tap { |h| h.default = 0 }

      users.each do |user|
        total = total_per_user[user.id]
        fulfill(user, total)
      end
    end
  end
end

class UserType < GraphQL::Schema::Object
  field :payments_total, Integer do
    argument :from, Time
    argument :to, Time
  end
end

query_string = <<~GQL
  {
    users {
      paymentsTotal
    }
  }
GQL

# No N+1. And never will be!
p Schema.execute(query_string, variables: {from: 3.days.ago, to: 1.day.ago})['data']

Define loaders once - use it everywhere without N+1.

Check N1Loader for more features and give it a try in your projects.

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments0

Articles