Graphql-ruby: Pre-loading/Eager loading nested associations

Created on 5 Nov 2015  路  8Comments  路  Source: rmosolgo/graphql-ruby

Hello @rmosolgo

Thank you for the Gem :)

Just been trying out GraphQL with rails and wondering, if I can eager load nested associations in parent type. For ex: If I have a model Post, which has many comments and then comment belongs to a user.

If I try to fetch this query:

query_string = "query getPostById { post(id: 1) {id, body, comments{body, user{id, email}}}}"
PostType = GraphQL::ObjectType.define do
  name "Post"
  description "A post entry"

  interfaces [NodeIdentification.interface]

  field :id, field: GraphQL::Relay::GlobalIdField.new('Post')
  field :title, types.String, "The title of this post"
  field :body, types.String,  "The body of this post"
  field :comments, -> { types[CommentType] }, "All comments of this post"
  field :user, UserType, "User associated with this post"

  field :comments_count do
    type types.Int
    description "Get comments count for this post"
    resolve -> (post, arguments, context) {
      post.comments.count
    }
  end

end

The above query runs 1 additional query per comment loading comment user. Normally, we would do like: post.comments.includes(:user), which would basically run two queries and get all comments plus its users.

Is I am missing anything?

Most helpful comment

@gauravtiwari To solve this, I access the child nodes within my resolve block to check if I will need to eager load that association:

if ctx.ast_node.children.map(&:name).include?('comments')

All 8 comments

Shopify has published a gem for this purpose! Check this one out: https://github.com/Shopify/graphql-batch (I haven't tried it yet)

thanks @rmosolgo will check this one and report usage.

@gauravtiwari To solve this, I access the child nodes within my resolve block to check if I will need to eager load that association:

if ctx.ast_node.children.map(&:name).include?('comments')

thanks @davidnorth for the tip :+1:

i wrote a really naive approach to this earlier today.

it could use a lot of work/refactoring, but maybe this will help someone/maybe people can provide feedback

also i can't imagine this works with fragments

class ActiveRecordField < GraphQL::Field
  [...]
  def resolve(object, arguments, ctx)
    includes = map_includes(@model, ctx.ast_node.selections)

    @model.includes(*includes).find(arguments["id"])
  end

  def map_includes(model, selections)
    selections.map do |selection|
      if selection.selections.present?
        singular = selection.name.singularize.to_sym
        plural   = selection.name.pluralize.to_sym

        nested = map_includes(singular.to_s.classify.constantize, selection.selections)

        final_type =  if model.reflections[singular].present?
                        # this is for has_one relationships
                        singular

                      elsif model.reflections[plural].present?
                        # this is for has_many relationships
                        plural
                      end

        if nested.present?
          { final_type => nested }
        else
          final_type
        end
      else
        nil
      end
    end.compact
  end
end

:+1: glad to see some good options! I don't want to build this feature into graphql but I definitely want to support it, please open another issue if there's something that would make it easier.

0.16.0 introduces ctx.irep_node. You can inspect the node's children to see _all_ fields which were queried and what types they'll apply to:

ctx.irep_node.children.each do |irep_child|
  irep_child.name # alias or field name 
  irep_child.definition # => <#GraphQL::Field ...> Field definition 
  irep_child.on_types # => Set<types...> Type definitions which this field will apply to
  irep_child.ast_node # => <#GraphQL::Language::Nodes::Field ... >
end 

I haven't had a ton of time to play around with it, but this is the data structure used for query execution now, I think it will provide better support for look-ahead too!

Was this page helpful?
0 / 5 - 0 ratings