rmosolgo / graphql-ruby

Ruby implementation of GraphQL
http://graphql-ruby.org
MIT License
5.37k stars 1.39k forks source link

Q: Prevent N+1 queries #189

Closed elado closed 8 years ago

elado commented 8 years ago

Let's say that I have a PostType and it has comments field and a query may or may not contain post { comments { ... } }.

Is there a proper way to inspect the query and see if it requires comments, so I can do Post.includes(:comments)? (Or even Post.includes(:comments => :user) in case of { post { comments { ... node { user } }

rmosolgo commented 8 years ago

As of 0.16 you can inspect ctx.irep_node to see fields which might be resolved.

field :post, PostType do 
  resolve -> (obj, args, ctx) {
    post = obj.post 
    # Check if any nodes are the "comments" field 
    has_comments = ctx.irep_node.children.any? { |node| node.definition.name == "comments" }
    if has_comments 
      post = post.includes(:comments)
    end 

    post 
  }
end 

You could continue recursively for user. It's a bit of a new feature so let me know how it works out for you!

rmosolgo commented 8 years ago

Another important bit is graphql-batch, helpful in some cases: https://github.com/Shopify/graphql-batch

elado commented 8 years ago

Thanks!

I ended up with this:

class GraphHelper
  def self.ctx_has_path?(ctx, *path)
    path.reduce(ctx.irep_node) do |curr_node, path_item|
      new_curr_node = curr_node.children.find { |k, v| v.definition_name == path_item.to_s }.try(:[], 1)
      return false if new_curr_node.nil?
      new_curr_node
    end.present?
  end
end

# ...

QueryType = GraphQL::ObjectType.define do
  # ...
  field :collection do
    type CollectionType
    argument :id, !types.String
    resolve -> (root, args, ctx) {
      type_name, id = NodeIdentification.from_global_id(args[:id])

      q = Collection
      q = q.includes(:items) if GraphHelper.ctx_has_path?(ctx, :items)
      q = q.includes(:items => :entity) if GraphHelper.ctx_has_path?(ctx, :items, :edges, :node, :entity)
      q.find(id)
    }
  end
end

CollectionType = GraphQL::ObjectType.define do
  name 'Collection'
  # ...
  connection :items do
    type CollectionItemType.connection_type
    resolve -> (root, args, ctx) {
      items = root.items
      items = items.includes(:entity) if GraphHelper.ctx_has_path?(ctx, :edges, :node, :entity)
      items
    }
  end
end
elado commented 8 years ago

graphql-batch is also super useful!

karloku commented 8 years ago

Instead of taking care about N+1 queries in resolvers, the query executor should handle them. Both

and

are right places to solve N + 1 queries.