Last active
October 31, 2023 07:15
-
-
Save theorygeek/a1a59a2bf9c59e4b3706ac68d12c8434 to your computer and use it in GitHub Desktop.
Preloading Associations with graphql-batch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
Schema = GraphQL::Schema.define do | |
# configuration stuff | |
instrument(:field, AssociationPreloadInstrumentation.new) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A large part of the credit for this gist goes is owing to this comment: Shopify/graphql-batch#24 (comment)