Skip to content

Graphql Batch gem to remove N+1

We are using this gem instead of eager loading because this will not preload the data until it is in use. It makes it easy to read logs. We have modules like association loader and active storage loader which are provided by gem but we changed them as per the requirement. The association loader is used to batch load all the associated data, it could be has_many, has_many through, or polymorphic relationship. We are using resolvers along with the modules to make code dry and readable.

How to use it for association

Through resolvers:

We have an association as the user has many accounts

field :accounts, [Types::AccountType], null: true, resolve: Resolvers::BatchResolver.load(:accounts)
Here, We are using the resolver module which is used to load the associated data.

Note: load method takes parameter as association name, field name could be anything but we need to provide the correct association name in the load method.

BatchResolver is a custom class that calls association loader from it

Through method batch_load()

Suppose we are using the associated relationship in user_type.rb file, Ex:

field :secondary_email, String, null: true
def secondary_email
  batch_load(object, :contact_infos).then do |contact_infos|
    secondary_details(contact_infos, 'email').first&.info
  end
end

To fetch secondary email we were using the object.contact_infos, this will run each time when the query is hit, and this was causing the n+1 problem. To resolve this we have the batch_load method that loads the associated data and resolves n+1. batch_load method calls association loader that takes care of loading. This method takes two parameters, the first one is the object and another one is the association name.

There might be a question why we are using two different ways if both are performing batch loading.
The answer is if there is direct loading then we must go for resolver but if we want to perform some operation on the loaded data then we will use the batch_load method.

Graphql batch gem uses promises therefore using .then with the batch load method helps to perform other operations. If there are multiple associations required to perform the operation on the field then we must use the nested batch_load method.Ex:

  field :payment_plan, Boolean, null: false

The payment plan will return true if there any active payment plan exists or there any plan payment is present. To perform this operation we were using :

object.payment_plans.active.present? || object.plan_payments.present?

This causes n+1 problem and to resolve such multiple associative relationships we use to perform nesting of batch load like this:

def payment_plan
  batch_load(object, :payment_plans).then do |payment_plans|
    batch_load(object, :plan_payments).then do |plan_payments|
      active_payment_plan?(payment_plans) || plan_payments.present?
    end
  end
end

How to use it for Active storage attachments

Graphql batch gem also provides us with the active storage loader class that handles the batch loading of attachments. We can batch load the attachments for both the association type: has_one and has_many. For this, we have the attachement_load method that performs batch loading for active storage attachments.

Syntax:

attachment_load(class_name_in_string, attachment_name, object_id, type: has_one_attached/has_many_attached, **args)
Ex: In note_type.rb

field :attachments, [GraphQL::Types::JSON], null: true
def attachments
  args = { where: 'status <> 1', order: 'created_at DESC' }
  type = :has_many_attached
  attachment_load('Notes::Note', :documents, object.id, type: type, **args).then do |documents|
    documents_attached = []
    documents.compact.select do |doc|
      file = {
        id: doc.id,
        filename: doc.blob.filename,
        url: host_url(doc),
        created_at: doc.created_at,
        task_id: doc.record_id,
        task_name: object.body,
        uploaded_by: ActiveStorage::Attachment.find_by(
          blob_id: doc.blob_id, record_type: 'Users::User',
        )&.record&.name,
        comment_count: object.note_comments.tagged_document_comments(doc.id).size,
      }
      documents_attached << file
    end
    documents_attached.empty? ? nil : documents_attached
  end
end
  • The default value of type is has_one_attached
  • args is used to perform SQL operations for where and order

Here we see, that the method attachment_load loads the documents and then we perform operations on the result data.

The attachment load method will return the array of documents and then we can perform operations on it.

For has_one relationship we have:

def thumbnail_url
  batch_load(object&.guest, :request).then do |request|
    attachment_load('Logs::EntryRequest', :video, request&.id).then do |video|
      host_url(video.preview(resize_to_limit: [300, 300]).processed.image) if video.present?
    end
  end
end

Here we batch load the request and then we loaded attachments with respect to that request.

For has_one relationship, attachment_load returns object of ActiveStorage Attachment for that request.

Few Things to take care of-

  • Avoid using associative queries in xtype.rb files as they are prone to n+1
  • If you are having multiple queries ex: attachments of note_type then it is difficult to perform batch_load for each and every query as we need to nest each and every batch load for performing operations in it. That increases complexity and downgrades readability.