ruby-on-railsgraphqllazy-loadinggraphql-ruby

Graphql Ruby N + 1 Queries because of method calling another relationship


I am using the graphql and batch-loader gems and running into this N+1 query:

I have a calendar appointment that belongs to an account, and when we display the appointment time we base it on the account's time zone.

def appointment_time
  read_attribute(:appointment_time).in_time_zone(self.account.timezone)
end
calendarAppts {
  appointmentTime
}

results in an N+1 query

POST /graphql
USE eager loading detected
  CalendarAppt => [:account]
  Add to your query: .includes([:account])
Call stack
  /....rb:866:in `appointment_time'
  /.../app/controllers/graphql_controller.rb:17:in `execute'

And I have many other similar examples of instance methods creating N+1 queries and unsure the best way to tackle this.

So far I have just seen ways to conquer eager loading associations directly. Do I need to treat each of such method as an association?

For instance this is how I eager load an account itself, do I need to do something similar when I call this method as well?

    field :account,
          AccountType,
          null: true,
          resolve: lambda { |obj, _args, _ctx|
            BatchLoader::GraphQL.for(obj.account_id).batch do |account_ids, loader|
              Account.where(id: account_ids).each { |account| loader.call(account.id, account) }
            end
          }

Also then if we did something like this would it call accounts twice?

calendarAppts {
  appointmentTime
  account {
    id
    name
  }
}

Solution

  • Assuming you're using graphql-ruby to handle your queries you should use lookahead to see what sub-fields are required and prepare your query accordingly.

    So in your case, in the method implementing callendarAppts, you should do something like:

    field :calendar_appts, ..., extras: [:lookahead]
    def callendar_appts(lookadead:)
      scope = Model.where(...)
      if(lookahead.selects?(:appointment_time))
         scope = scope.includes([:accounts])
      end
      scope.all
    end
    

    (I'm spit-balling here it as the actual implementation has not been provided)