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)
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)
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.