fs / rails-base-graphql-api

Base Rails application for GraphQL API
27 stars 7 forks source link

Optimize database SQL queries using GraphQL::Execution::Lookahead #152

Open sergeyantonov1 opened 3 years ago

sergeyantonov1 commented 3 years ago

What is lookahead? https://graphql-ruby.org/queries/lookahead.html#getting-a-lookahead

How we can implement it? In the example below recent_assessments association will be loaded if can_make_assessment will be selected in GraphQL request.

# app/graphql/resolvers/base.rb
module Resolvers
  class Base < GraphQL::Schema::Resolver
    include ActionPolicy::GraphQL::Behaviour

    argument_class Types::BaseArgument

    def resolve(**options)
      @options = options
      fetch_data
    end

    private

    attr_reader :options

    def fetch_data
      raise NotImplementedError
    end

    def current_user
      @current_user ||= context[:current_user]
    end

    def preload_associations
      lookahead_selection_names.each_with_object([]) do |selection, associations|
        associations << self.class::PRELOAD_ASSOCIATIONS_MAPPING[selection]
      end.flatten.compact
    end

    def lookahead_selection_names
      options[:lookahead].selections.map { |s| s.name.to_sym }
    end
  end
end

# app/graphql/resolvers/users.rb
module Resolvers
  class Users < Resolvers::Base
    # some arguments

    extras [:lookahead]

    type [Types::UserType], null: true

    PRELOAD_ASSOCIATIONS_MAPPING = {
      can_make_assessment: [:recent_assessments]
    }.freeze

    def fetch_data
      sorted_relation(filtered_relation)
    end

    private

    def filtered_relation
      FilteredUsersQuery.new(relation, options).all
    end

    def relation
      object.users.includes(preload_associations)
    end
  end
end
ArtemMotrych commented 3 years ago

image

Now in gfaphql base project used gem https://github.com/Shopify/graphql-batch for load associations. That gem in rare cases generate extra sql request like on screenshoot. We have 2 variants to resolve that problem - fix graphql-batch problem or try lookahead that @sergeyantonov1 refer in comments on top.

sergeyantonov1 commented 3 years ago

I've updated the preparing associations logic a little bit cuz implementation that I provided above doesn't work correctly with nested associations. If you have a more readable or simple solution welcome.

module Resolvers
  class Base < GraphQL::Schema::Resolver
    include ActionPolicy::GraphQL::Behaviour
    include SortedRelation

    argument_class Types::BaseArgument

    def resolve(**options)
      @options = options
      fetch_data
    end

    private

    attr_reader :options

    def fetch_data
      raise NotImplementedError
    end

    def current_user
      @current_user ||= context[:current_user]
    end

    def preload_associations
      [prepared_single_associations, prepared_nested_associations].flatten.compact
    end

    def prepared_nested_associations
      raw_preload_associations.reduce({}) do |hash, element|
        if element.is_a?(Hash)
          hash.merge(element) { |_, v1, v2| (v1 + v2).uniq }
        else
          hash
        end
      end.presence
    end

    def prepared_single_associations
      raw_preload_associations.map { |association| association if association.is_a?(Symbol) }.compact
    end

    def raw_preload_associations
      @raw_preload_associations ||=
        lookahead_selection_names.each_with_object([]) do |selection, associations|
          associations << self.class::PRELOAD_ASSOCIATIONS_MAPPING[selection]
        end.flatten.compact
    end

    def lookahead_selection_names
      options[:lookahead].selections.map { |s| s.name.to_sym }
    end
  end
end

These changes will work with nested associations.

For example, we have next mapping:

PRELOAD_ASSOCIATIONS_MAPPING = {
  can_make_assessment: [recent_received_feedbacks_with_assessment: [:sender]],
  fired_at: [:sent_feedbacks, recent_received_feedbacks_with_assessment: [:recipient]]
}.freeze

In this case, preload associations will equal to:

[:sent_feedbacks, {:recent_received_feedbacks_with_assessment=>[:recipient, :sender]}]

How it will work without this patch:

[:sent_feedbacks, {:recent_received_feedbacks_with_assessment=>[:recipient]}, {:recent_received_feedbacks_with_assessment=>[:sender]}]