Skip to content

Instantly share code, notes, and snippets.

@theorygeek
Last active October 31, 2023 07:15
Show Gist options
  • Save theorygeek/a1a59a2bf9c59e4b3706ac68d12c8434 to your computer and use it in GitHub Desktop.
Save theorygeek/a1a59a2bf9c59e4b3706ac68d12c8434 to your computer and use it in GitHub Desktop.
Preloading Associations with graphql-batch
# frozen_string_literal: true
class AssociationLoader < GraphQL::Batch::Loader
attr_reader :klass, :association
def initialize(klass, association)
raise ArgumentError, "association to load must be a symbol (got #{association.inspect})" unless association.is_a?(Symbol)
raise ArgumentError, "cannot load associations for class #{klass.name}" unless klass < ActiveRecord::Base
raise TypeError, "association #{association} does not exist on #{klass.name}" unless klass.reflect_on_association(association)
@klass = klass
@association = association
end
def load(model)
raise TypeError, "loader for #{klass.name} can't load associations for objects of type #{model.class.name}" unless model.is_a?(klass)
model.association(@association).loaded? ? Promise.resolve(model) : super
end
def perform(models)
ActiveRecord::Associations::Preloader.new.preload(models, association)
models.each { |m| fulfill(m, m) }
end
end
# frozen_string_literal: true
class AssociationPreloadInstrumentation
def instrument(_type, field)
return field unless field.metadata.include?(:preload)
old_resolver = field.resolve_proc
new_resolver = -> (object, args, ctx) do
preload(object, field.metadata[:preload]).then do
old_resolver.call(object, args, ctx)
end
end
field.redefine do
resolve(new_resolver)
end
end
private
def preload(object, associations)
if associations.is_a?(Symbol)
preload_single_association(object, associations)
else
promises = []
Array.wrap(associations).each do |value|
case value
when Symbol
promises << preload_single_association(object, value)
when Array
value.each { |sub_value| promises << preload(object, sub_value) }
when Hash
value.each do |key, sub_value|
promises << preload_single_association(object, key).then do
next_value = object.public_send(key)
case next_value
when ActiveRecord::Base
preload(next_value, sub_value)
else
Promise.all(Array.wrap(next_value).map { |next_model| preload(next_model, sub_value) })
end
end
end
end
end
Promise.all(promises)
end
end
def preload_single_association(object, association)
return Promise.resolve(object) if object.association(association).loaded?
AssociationLoader.for(object.class, association).load(object)
end
end
# frozen_string_literal: true
# This goes in a Rails initializer, or some other code that runs before your schema/types are required
GraphQL::Field.accepts_definitions(
preload: -> (type, *args) do
type.metadata[:preload] ||= []
type.metadata[:preload].concat(args)
end
)
PostType = GraphQL::ObjectType.define do
name "Post"
field :comments, !types[!CommentType] do
preload comments: :author
resolve -> (post, args, ctx) { post.comments }
end
# Or you can use the more terse syntax:
field :comments, !types[!CommentType], preload: { comments: :author }, property: :comments
end
# frozen_string_literal: true
Schema = GraphQL::Schema.define do
# configuration stuff
instrument(:field, AssociationPreloadInstrumentation.new)
end
@theorygeek
Copy link
Author

A large part of the credit for this gist goes is owing to this comment: Shopify/graphql-batch#24 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment