shopify / graphql-batch Goto Github PK
View Code? Open in Web Editor NEWA query batching executor for the graphql gem
License: MIT License
A query batching executor for the graphql gem
License: MIT License
We load some records to place into the graphql context before running a query. I'd like to warm the graphql batch loader caches with these records. At the moment I'm doing something like this (with a contrived example):
# app/graphql/record_loader.rb
class RecordLoader < GraphQL::Batch::Loader
def initialize(record_class)
@record_class = record_class
@primary_key = record_class.primary_key
end
def perform(keys)
# Resolve unfulfilled keys
records_by_key = @record_class.where(@primary_key => keys).index_by(&@primary_key)
# Fulfills both found keys as records, and unfound keys as nil
keys.each { |key| fulfill(key, records_by_key[key]) }
end
def warm(record)
key = record.public_send(@primary_key)
promise = cache[cache_key(key)] ||= Promise.new.tap { |promise| promise.source = self }
promise.fulfill(record) unless promise.fulfilled?
end
end
# app/graphql/schema.rb
Schema = GraphQL::Schema.define do
# ...
use GraphQL::Batch
use GraphQL::Batch::Warmer, with: -> (query) do
RecordLoader.for(User).warm(query.context[:current_user])
end
# ...
end
# lib/graphql/batch/warmer.rb
module GraphQL::Batch::Warmer
def self.use(schema_defn, with:)
schema_defn.instrument(:query, QueryInstrumenter.new(with))
end
class QueryInstrumenter
def initialize(block)
@block = block
end
def before_query(query)
unless GraphQL::Batch::Executor.current
raise "Move `use GraphQL::BatchWarmer` below `use GraphQL::Batch` in your schema, or remove it"
end
@block.call(query)
end
def after_query(query)
end
end
end
This feels like it dips a little too much into this gem's responsibilities. Is there any interest including some sort of cache warming facility in this gem?
Update: I've simplified the example.
Hi everyone!
First, i would like to thank you for your awesome job on the OSS community!
I've probably a bug using graphql-batch
. I've duplicate reads of replacements
associated records when i use the AssociationLoader
class, only after a succeed mutation, not when i use a query. If i disable the batch it works well.
Any idea of what's going bad?
data: {,…}
createMandate: {...}
errors: []
mandate: {…}
[...]
replacements: [{id: "144", flexibleTime: false, endsAt: "2019-04-12T01:00:00Z", priority: false,…},…]
0: {id: "144", flexibleTime: false, endsAt: "2019-04-12T01:00:00Z", priority: false,…}
1: {id: "145", flexibleTime: false, endsAt: "2019-04-13T01:00:00Z", priority: false,…}
2: {id: "144", flexibleTime: false, endsAt: "2019-04-12T01:00:00Z", priority: false,…}
3: {id: "145", flexibleTime: false, endsAt: "2019-04-13T01:00:00Z", priority: false,…}
[...]
success: true
# app/models/mandate.rb
class Mandate < ApplicationRecord
has_many :replacements, class_name: 'Replacement', autosave: true, dependent: :destroy
has_many :proposals, through: :replacements, class_name: 'Proposal'
accepts_nested_attributes_for :replacements, allow_destroy: true
end
# app/graphql/types/mandate.rb
class Types::Mandate < Types::BaseObject
[..]
field :replacements, [::Types::Replacement], "Mandate's replacements", null: false
[..]
def replacements
AssociationLoader.for(Mandate, :replacements).load(object)
end
end
# app/graphql/association_loader.rb
class AssociationLoader < GraphQL::Batch::Loader
def self.validate(model, association_name)
new(model, association_name)
nil
end
def initialize(model, association_name)
@model = model
@association_name = association_name
validate
end
def load(record)
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
return Promise.resolve(read_association(record)) if association_loaded?(record)
super
end
# We want to load the associations on all records, even if they have the same id
def cache_key(record)
record.object_id
end
def perform(records)
preload_association(records)
records.each { |record| fulfill(record, read_association(record)) }
end
private
def validate
unless @model.reflect_on_association(@association_name)
raise ArgumentError, "No association #{@association_name} on #{@model}"
end
end
def preload_association(records)
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
end
def read_association(record)
record.public_send(@association_name)
end
def association_loaded?(record)
record.association(@association_name).loaded?
end
end
Hey everyone,
i am searching for a possibility use the Loader for other columns then the key-column to work with the following case:
I have some ContactModes and every ContactMode is either an Email or a Telephone number, the problem with the Loader is that the foreign-key is in the other table so I have to load email.contact_mode_id == self.id instead of email.id == contact_mode.email_id
In Types::ContactModeType:
field :email do
type types[Types::EmailType]
resolve -> (contact_mode, args, ctx){
Email.where(contact_mode_id: contact_mode.id)
}
end
field :address do
type types[Types::AddressType]
resolve -> (contact_mode, args, ctx) {
Address.where(contact_mode_id: contact_mode.id)
}
end
Is there any possibility to call the loader use Table.column instead of Table.id for Loading?
Thanks for answer
Given the following query
query profileRefQuery {
me {
profile_visits {
collection {
number_of_visits
visitor_id
visitor {
...profileFragment
}
}
total
}
}
profile_refs(ids: [1]){
... profileFragment
}
}
on the following type system
Me = GraphQL::ObjectType.define do
name "me"
field :profile_ref do
type Types::Profiles::ProfileRef
resolve Resolvers::Profiles::ProfileRef
end
end
QueryType = GraphQL::ObjectType.define do
name "Query"
field :profile_refs do
type types[!Types::Profiles::ProfileRef]
argument :ids, !types.ID.to_list_type
resolve ->(obj, args, context) {
Promise.all(args["ids"].map { |id| Resolvers::Profiles::ProfileRef.call(obj, {"id" => id}, context) })
}
end
field :me do
type Me
resolve ->(_,_,_){ {} }
end
end
how would I reasonably indicate errors? My Batch::Loader
is calling into a different system via HTTP/Rest, which can of course have timeouts.
Currently I raise an GraphQL::ExecutionError
from my perform(ids)
method when it runs into a timeout. That sort of works.
The only nasty side effect of this is that in the response all paths are wrong (because they point to the resolved promise).
{
"data": {
"me": {
"profile_visits": {
"collection": [
{
"number_of_visits": 2,
"visitor_id": "2",
"visitor": null
},
{
"number_of_visits": 2,
"visitor_id": "15",
"visitor": null
},
{
"number_of_visits": 1,
"visitor_id": "2",
"visitor": null
},
{
"number_of_visits": 1,
"visitor_id": "15",
"visitor": null
}
],
"total": 4
}
},
"profile_refs": null
},
"errors": [
{
"message": "BACKEND_TIMEOUT",
"locations": [
{
"line": 29,
"column": 3
}
],
"path": [
"profile_refs"
],
"detail": {}
},
{
"message": "BACKEND_TIMEOUT",
"locations": [
{
"line": 29,
"column": 3
}
],
"path": [
"profile_refs"
],
"detail": {}
},
{
"message": "BACKEND_TIMEOUT",
"locations": [
{
"line": 29,
"column": 3
}
],
"path": [
"profile_refs"
],
"detail": {}
},
{
"message": "BACKEND_TIMEOUT",
"locations": [
{
"line": 29,
"column": 3
}
],
"path": [
"profile_refs"
],
"detail": {}
},
{
"message": "BACKEND_TIMEOUT",
"locations": [
{
"line": 29,
"column": 3
}
],
"path": [
"profile_refs"
],
"detail": {}
}
]
}
Which basically means that a client can't detect that resolving the visitor has failed.
How would you handle this? Any suggestions?
In graphql-ruby
, I've found that every single field of a type is not asynchronizly resolved
# the sleep method represents any blocking method call or IO
field :a, types.String do
resolve -> (o, args, c) {
sleep 3
"a"
}
end
field :b, types.String do
resolve -> (o, args, c) {
sleep 3
"b"
}
end
field :c, types.String do
resolve -> (o, args, c) {
sleep 3
"c"
}
end
field :d, types.String do
resolve -> (o, args, c) {
sleep 3
"d"
}
end
if a query like
test {
a
b
c
d
}
the time cost could be
Completed 200 OK in 12602ms (Views: 0.6ms)
seems like the time cost is 3 * 4 = 12 second
I want to get the field concurrently, then the time cost could be nearly 3 second. Is it a good way to use your gem ? Or I should submit a new issue in graphql-ruby 😂
Is there any tool like bullet gem to detect N+1 queries in GraphQL resolvers. Does Bullet gem will detect it?
There is a Namespacing inconsistency in GraphQL::Batch::Loader
between the Promises returned by load
and load_many
causing them to return different classes.
def load(key)
cache[cache_key(key)] ||= begin
queue << key
Promise.new.tap { |promise| promise.source = self } ### returns GraphQL::Batch::Promise
end
end
def load_many(keys)
::Promise.all(keys.map { |key| load(key) }) ### returns Promise
end
Is there a particular reason for this? I can't seem to reason one.
After upgrading to the latest versions of graphql and graphql-batch, the resolvers fail to work when they resolve to a lazy value.
› rg graphql Gemfile.lock
154: graphql (1.9.1)
155: graphql-batch (0.4.0)
Error:
NoMethodError: undefined method `extension' for nil:NilClass
graphql-batch-0.4.0/lib/graphql/batch.rb:25:in `block in use'
graphql-batch-0.4.0/lib/graphql/batch.rb:23:in `each'
graphql-batch-0.4.0/lib/graphql/batch.rb:23:in `use'
graphql-1.9.1/lib/graphql/define/defined_object_proxy.rb:28:in `use'
graphql-1.9.1/lib/graphql/schema.rb:781:in `block (2 levels) in to_graphql'
graphql-1.9.1/lib/graphql/schema.rb:777:in `each'
graphql-1.9.1/lib/graphql/schema.rb:777:in `block in to_graphql'
graphql-1.9.1/lib/graphql/define/instance_definable.rb:161:in `instance_eval'
graphql-1.9.1/lib/graphql/define/instance_definable.rb:161:in `ensure_defined'
graphql-1.9.1/lib/graphql/schema.rb:265:in `define'
graphql-1.9.1/lib/graphql/define/instance_definable.rb:130:in `redefine'
graphql-1.9.1/lib/graphql/schema.rb:776:in `to_graphql'
graphql-1.9.1/lib/graphql/schema/member/accepts_definition.rb:120:in `to_graphql'
graphql-1.9.1/lib/graphql/schema.rb:718:in `graphql_definition'
graphql-1.9.1/lib/graphql/schema/field.rb:627:in `with_extensions'
graphql-1.9.1/lib/graphql/schema/field.rb:485:in `resolve'
Cause seems to be #99, and this code:
graphql-batch/lib/graphql/batch.rb
Lines 20 to 25 in 77945d4
The metadata
is empty ({}
) on the field, so it cannot load a value to call #extension
on. Making the if
not match, so that part is skipped, makes everything work again.
The type that caused this error is defined using the new class-based syntax, but not all types in the schema is using that syntax. I don't know if it's relevant or not.
This happens both in my test setup (that uses some fixtures and stubs) and with real queries against the schema, so I don't think it's caused by any weird setup on my end.
We do not use the Interpreter and the schema is set up this way:
module MyApiName
class Schema < GraphQL::Schema
query QueryRoot
mutation MutationRoot
use GraphQL::Batch
use GraphQL::Backtrace
end
end
Moving use
on top of the other types (which is mentioned as a bad thing in #99) does not change anything either.
Is there anything else I can do here to help debug this? Is there a workaround I can use?
I just saw that version 0.3 was released- congratulations! Is there a list of what has changed? I would recommend also checking it into the repo in a changelog file.
Cheers
There was some discussion about how one might preload has_many
associations in #graphql
.
It might be interesting to share some of our AssociationLoader
logic or writing a quick tutorial to achieve this in the README
.
I don't know if this is a bug or just a dumb question, but let me explain it 😄
Starting with this simple type:
class Types::ProjectType < Types::BaseNode
graphql_name 'Project'
field :title, String, null: false
field :milestones, Types::MilestoneType.connection_type, null: false
def milestones
object.milestones
end
end
And running this query:
query {
projects(perPage: 10) {
edges {
node {
title
milestones {
edges {
node {
title
}
}
}
}
}
}
}
Returns the desired result, but leads to this:
Milestone Load (0.9ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 1]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 2]]
Milestone Load (0.7ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 3]]
Milestone Load (0.6ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 4]]
Milestone Load (0.6ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 5]]
Milestone Load (0.7ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 6]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 7]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 8]]
Milestone Load (0.8ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 9]]
Milestone Load (0.8ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 10]]
So I thought it would be good to use the AssociationLoader to eager load the milestones using this:
def milestones
Loaders::AssociationLoader.for(Project, :milestones).load(object)
end
But this ends up in this:
Milestone Load (0.7ms) SELECT "milestones".* FROM "milestones" WHERE "milestones"."project_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) [["project_id", 1], ["project_id", 2], ["project_id", 3], ["project_id", 4], ["project_id", 5], ["project_id", 6], ["project_id", 7], ["project_id", 8], ["project_id", 9], ["project_id", 10]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 1]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 2]]
Milestone Load (0.4ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 3]]
Milestone Load (0.7ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 4]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 5]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 6]]
Milestone Load (1.0ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 7]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 8]]
Milestone Load (0.5ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 9]]
Milestone Load (0.6ms) SELECT milestones.*, milestones.id AS cursor_0 FROM "milestones" WHERE "milestones"."project_id" = $1 ORDER BY milestones.id asc [["project_id", 10]]
Completed 200 OK in 497ms (Views: 0.4ms | ActiveRecord: 34.6ms)
user: peter_goebel
POST /graphql
AVOID eager loading detected
Project => [:milestones]
Remove from your finder: :includes => [:milestones]
Call stack
Why are all milestones still queried individually even with eager loading/preloading?
Hello,
Is it okay to use a struct, hash or object as loader key?
Loader.for(Product, key: :id).load(obj) # pass whole object to loader
At first it does not sound like a good idea. I want the object to be available inside the loader and perform authorization there. Sometimes you need to query for another resource during authorization and to avoid N+1 it has to happen inside the loader. I can provide more details about the issue but the answer would be appreciated 👍
Perhaps something like this would be pretty useful.
Loader.for(Product).load(obj.id, obj) # optionally pass object
The 1.10.x
development branch of graphql-ruby has introduced an issue with graphql-batch.
|| Failure/Error: use GraphQL::Batch
||
|| NoMethodError:
|| undefined method `target' for ApiSchema:Class
|| # /Users/damon/.gem/ruby/2.6.3/gems/graphql-batch-0.4.1/lib/graphql/batch.rb:19:in `use'
|| # /Users/damon/.gem/ruby/2.6.3/bundler/gems/graphql-ruby-432465c39f72/lib/graphql/schema.rb:839:in `use'
|| # ./app/graphql/api_schema.rb:19:in `<class:ApiSchema>'
|| # ./app/graphql/api_schema.rb:3:in `<main>'
@swalkinshaw suggests to conditionally check if schema_defn
responds to target
at: https://github.com/Shopify/graphql-batch/blob/master/lib/graphql/batch.rb#L19
I am not sure I fully understand what is going on with schema
and how to approach it when target
is not present. Happy to help if someone can point me in the right direction.
Originally posted this at graphql-ruby but was suggested I post it here.
M Gemfile.lock
@@ -1,9 +1,9 @@
GIT
remote: git://github.com/rmosolgo/graphql-ruby.git
- revision: 87942848cf1f01ca5a54d069da41e286cab137d0
- ref: 1.10-dev
+ revision: 432465c39f72aa7fb108f9e66d94fea6b0e2d4af
+ tag: v1.10.0.pre2
specs:
- graphql (1.10.0.pre1)
+ graphql (1.10.0.pre2)
Relay support in graphql-ruby suggests resolvers for a connection_type
should be able to return an array. Expect that graphql-batch
would support field resolver returning promise of an array in this scenario. It instead fails with exception No connection implementation to wrap GraphQL::Batch::Promise
.
A workaround is to have the resolver wait for promise fulfillment by calling Promise#sync on the Loader#load/#load_many
result.
For a repro, consider this script using graphql (0.19.4)
and graphql-batch (0.2.4)
on ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
:
require 'graphql/batch'
class ShopLoader < GraphQL::Batch::Loader
def initialize(ctx)
end
def perform(ids)
ids.each { |id| fulfill(id, OpenStruct.new({ id: id })) }
end
end
class ShopResolver
def self.call(obj, args, ctx)
ShopLoader.for("ctx").load_many([ 1, 2, 3 ])
end
end
ShopType = GraphQL::ObjectType.define do
name "Shop"
field :id do
type types.ID
end
end
QueryType = GraphQL::ObjectType.define do
name "Query"
field :shops do
type types[ShopType]
resolve ShopResolver
end
connection :shopsConnection, ShopType.connection_type do
resolve ShopResolver
end
end
Schema = GraphQL::Schema.define do
query QueryType
end
Schema.query_execution_strategy = GraphQL::Batch::ExecutionStrategy
Schema.mutation_execution_strategy = GraphQL::Batch::MutationExecutionStrategy
puts "Array use of cache loader:"
puts Schema.execute("{ shops { id } }")
# {"data"=>{"shops"=>[{"id"=>"1"}, {"id"=>"2"}, {"id"=>"3"}]}}
puts "Connection use of cache loader:"
puts Schema.execute("{ shopsConnection { edges { node { id } } } }")
# /Users/jocarrie/.rvm/gems/ruby-2.3.0/gems/graphql-0.19.4/lib/graphql/relay/base_connection.rb:37:in `connection_for_nodes':
# No connection implementation to wrap GraphQL::Batch::Promise (#<GraphQL::Batch::Promise:0x007f8081152170>) (RuntimeError)
# from /Users/jocarrie/.rvm/gems/ruby-2.3.0/gems/graphql-0.19.4/lib/graphql/relay/connection_resolve.rb:12:in `call'
This library is fantastic and thanks for publishing it!
In my testing, IDs are not always aggregated across multiple queries when Schema#multiplex
is used. The promises do resolve correctly, but one DB query is made per multiplexed gql query.
I found a test in this repo, which runs queries with multiplex
and uses QueryNotifier.subscriber
to assert that only 1 query is made, but my loader does not behave.
Current gem versions:
gem "graphql", "=1.6.0"
gem "graphql-batch", "=0.3.9"
The loader in question is very simple:
module Loaders
class Single < ::GraphQL::Batch::Loader
attr_reader :model, :options, :key
def initialize(model_arg, options_arg = {})
@model = model_arg
@options = options_arg
@key = options.fetch(:key, :id)
end
def perform(ids)
model.where(key => ids).all.each(&method(:fulfill_record))
ids.reject(&method(:fulfilled?)).each(&method(:fulfill_nil))
end
private
def fulfill_record(record)
fulfill(record.send(key), record)
end
def fulfill_nil(id)
fulfill(id, nil)
end
end
end
Hi, I'm getting the following error with my code:
KeyError (key not found: 1):
app/graphql/loaders/record_loader.rb:9:in `block in perform'
app/graphql/loaders/record_loader.rb:9:in `perform'
app/controllers/graphql_controller.rb:10:in `execute'
I can't quite figure out what I'm doing wrong, since the code is pretty much copied from the docs.
record_loader.rb:
require 'graphql/batch'
class Loaders::RecordLoader < GraphQL::Batch::Loader
def initialize(model)
@model = model
end
def perform(ids)
@model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
and my query resolver:
field :schoolById, Types::SchoolType do
description "Get school by id"
argument :id, types.ID
resolve -> (obj, args, context) {
Loaders::RecordLoader.for(School).load_many(args["id"])
}
end
I've double checked my code, and args["id"]
is a single element, not an array, so #50 isn't the issue. Guessing by the error message, the issue is in fulfill, but I'm not sure how or why. It's probably something small or dumb that I'm missing haha.
Thanks for your time,
Nicholas
Hi:
My graphql-ruby version is 1.9.6, and I use the association loader in the example directory
I have these models: Route, RoutesStop, and Stop
Route has this association:
has_many :stops, -> { order 'route_stops.order' }, through: :route_stops
When I use association loader to load stops
field in my RouteType, I got this kind of issue:
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR: missing FROM-clause entry for table "route_stops"
and the sql it actually execute is:
SELECT "stops".* FROM "stops" WHERE "stops"."id" IN (1, 12) ORDER BY route_stops.order
How can I unscope the order scope of ActiveRecord::Relation to avoid the exception OR what can I do to deal with the exception, under the premise of not removing the scope in the has_many relationship definition
Thanks!
How would one go to keep the order of records in the output? I have an ordered
scope for has_many
, but batch loading throws it off, and I don't want to sort in front-end.
The resolve_type seems to get the promise rather than the solved value:
Target = GraphQL::UnionType.define do
name 'MyTarget'
possible_types [X::Type, Y::Type]
resolve_type ->(obj, _ctx) do
binding.pry # -> obj.class == GraphQL::Batch::Promise
case o
when X then X::Type
when Y then Y::Type
end
end
end
Type = GraphQL::ObjectType.define do
name 'MyType'
field :id, !GraphQL::Common::ObjectId
field :targets, !types[!Target] do
resolve ->(obj, _args, _ctx) do
case obj.target_type
when 'X' then obj.targets.map { |t| RecordLoader.for(X).load(t) }
when 'Y' then obj.targets.map { |t| RecordLoader.for(Y).load(t) }
end
end
end
I'm using the base example with RecordLoader, like this in a resolver
Loaders::RecordLoader.for(Product).load(Product.ids)
I'm getting this error:
KeyError (key not found: 6)
6
is the first id loaded. I think its somehow related to promise_for(load_key)
in loader.rb.
Has anyone had this happen?
remove me
While I love that the A+ Promise gives us the ability to compose things in a functional context, I found that the current implementation requires quite a bit of monkey-patching and additional types/interfaces to take advantage of the natural concurrency that futures typically give us. Would the team be open to a PR around a basic future/task/IO type that can take advantage of nonblocking IO?
GraphQL queries are a natural fit for running concurrent fetches at each layer: multiple independent Active Record queries, web requests (e.g., Elasticsearch), etc. The interface could even be the same as Promise
(though I'd recommend that #then
be sugar for #map
and #flat_map
, and a few additional methods to make things more predictable/composable).
Here's an example for a single resolver:
resolve ->(_, inputs, ctx) {
Task
.all(current_user_required_task(ctx), relay_node_task(inputs[:categoryId], ctx))
.then { |user, category| create_project_task(user, category, inputs[:description]) }
.then { |project| { project: project } }
}
# *_task naming for explicitness
This mutation can fire off Active Record queries for the current user and category at the same time using Thread
. As soon as one fails it short-circuits by raising an exception and it doesn't need to wait for the other query to complete. When both queries succeed, it can immediately flat-map onto another task that creates the project, which maps into the final return value.
A library like GraphQL Batch could further take advantage of concurrency by using Task.all
internally at each nested node of the query tree. Fulfilled tasks are memoized, so later queries can be reduced to avoid fetching records that earlier queries have already fetched and combine them afterwards.
There's lots of potential here, but a bit of work to get there. I'm curious about community interest, though, seeing as we're beginning to explore these benefits internally.
In the README, a CountLoader
example is named, but it's not particularly detailed:
CountLoader.for(Shop, :smart_collections).load(context.shop_id),
CountLoader.for(Shop, :custom_collections).load(context.shop_id),
A clear example that demonstrates "You can also batch other stuff, not only load records from a DB" could be very helpful for new users. A great example would be something that does a HTTP request to some external API, combining two non-record-from-db-loading use cases.
While investigating an issue I noticed that when using graphql-batch
, a non-nullable field that raises a GraphQL::ExecutionError
causes an uncaught GraphQL::InvalidNullError
exception.
I have yet to find the exact commit in graphql-ruby
that caused this regression, but it's reproducible with the following diff:
diff --git a/test/batch_test.rb b/test/batch_test.rb
index daf48f0..4234c92 100644
--- a/test/batch_test.rb
+++ b/test/batch_test.rb
@@ -79,6 +79,22 @@ class GraphQL::BatchTest < Minitest::Test
assert_equal ["Product/123"], queries
end
+ def test_non_null_that_raises
+ query_string = <<-GRAPHQL
+ {
+ product(id: "1") {
+ id
+ nonNullButRaises {
+ id
+ }
+ }
+ }
+ GRAPHQL
+ result = Schema.execute(query_string)
+ end
+
def test_batched_association_preload
query_string = <<-GRAPHQL
{
diff --git a/test/support/schema.rb b/test/support/schema.rb
index c6eb843..aa97340 100644
--- a/test/support/schema.rb
+++ b/test/support/schema.rb
@@ -55,6 +55,13 @@ ProductType = GraphQL::ObjectType.define do
Promise.all([query]).then { query.value.size }
}
end
+
+ field :nonNullButRaises do
+ type !ImageType
+ resolve -> (_, _, _) {
+ raise GraphQL::ExecutionError, 'Error'
+ }
+ end
At first I thought it was graphql-ruby
that didn't handle this case correctly, but then I tried it out and it worked as expected.
I believe in this case you'd expect result
to be something along the lines of:
{
"data": {
"products": null
},
"errors": [
...
]
}
@dylanahsmith any idea? If not, I can take a look later this week.
I wasn't able to figure that out from the documentation (and quick glances in the code), could you please clarify a bit here?
How does GraphQL::Batch::Loader
interplay with graphql-ruby's
resolve context. I don't see any way of accessing it in the GraphQL::Batch::Loader
.
Is that intentional because you would loose the ast node context when batching? If the context is not accessible, what's the suggested way of dealing with context data (like the current user_id or something comparable)?
1.9 offers a new optional interpreter and AST-based analysis which has breaking changes. Field instrumentation won't be supported and graphql-batch uses it for mutations:
graphql-batch/lib/graphql/batch/setup.rb
Lines 12 to 25 in d1244b7
Ideally we'd support both and conditionally check the gem version. Here's one idea on how to support 1.9 borrowed from @rmosolgo :
def resolve_with_support(**kwargs)
GraphQL::Batch::Executor.current.clear
result = super
context.schema.sync_lazy(result)
ensure
GraphQL::Batch::Executor.current.clear
end
We'll have to figure out a good way of hooking into resolve_with_support
. I'm not sure it's possible without some manual intervention (including a module for example).
Can you explain this to me a little bit more?
# We want to load the associations on all records, even if they have the same id
def cache_key(record)
record.object_id
end
Which situation are we talking about here?
I'm seeing this stack trace when I use graphql (1.7.5
) and graphql-batch (0.3.8
) together with subscriptions:
NoMethodError - undefined method `clear' for nil:NilClass:
graphql-batch (0.3.8) lib/graphql/batch/setup.rb:22:in `ensure in block (2 levels) in
graphql-batch (0.3.8) lib/graphql/batch/setup.rb:22:in `block (2 levels) in
graphql (1.7.5) lib/graphql/relay/mutation/resolve.rb:18:in `call'
graphql (1.7.5) lib/graphql/field.rb:230:in `resolve'
graphql (1.7.5) lib/graphql/execution/execute.rb:254:in `call'
graphql (1.7.5) lib/graphql/schema/middleware_chain.rb:45:in `invoke_core'
graphql (1.7.5) lib/graphql/schema/middleware_chain.rb:38:in `invoke'
graphql (1.7.5) lib/graphql/execution/execute.rb:108:in `resolve_field'
graphql (1.7.5) lib/graphql/execution/execute.rb:72:in `block (2 levels) in
graphql (1.7.5) lib/graphql/tracing.rb:56:in `block in trace'
graphql (1.7.5) lib/graphql/tracing.rb:70:in `call_tracers'
graphql (1.7.5) lib/graphql/tracing.rb:56:in `trace'
graphql (1.7.5) lib/graphql/execution/execute.rb:71:in `block in resolve_selection'
graphql (1.7.5) lib/graphql/execution/execute.rb:64:in `resolve_selection'
graphql (1.7.5) lib/graphql/execution/execute.rb:36:in `block in resolve_root_selection'
graphql (1.7.5) lib/graphql/tracing.rb:56:in `block in trace'
graphql (1.7.5) lib/graphql/tracing.rb:70:in `call_tracers'
graphql (1.7.5) lib/graphql/tracing.rb:56:in `trace'
graphql (1.7.5) lib/graphql/execution/execute.rb:32:in `resolve_root_selection'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:108:in `begin_query'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:81:in `block in run_as_multiplex'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:80:in `run_as_multiplex'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:69:in `block (2 levels) in run_queries'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:185:in `with_instrumentation'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:68:in `block in run_queries'
graphql (1.7.5) lib/graphql/tracing.rb:56:in `block in trace'
graphql (1.7.5) lib/graphql/tracing.rb:70:in `call_tracers'
graphql (1.7.5) lib/graphql/tracing.rb:56:in `trace'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:58:in `run_queries'
graphql (1.7.5) lib/graphql/execution/multiplex.rb:48:in `run_all'
graphql (1.7.5) lib/graphql/schema.rb:299:in `block in multiplex'
graphql (1.7.5) lib/graphql/schema.rb:606:in `with_definition_error_check'
graphql (1.7.5) lib/graphql/schema.rb:298:in `multiplex'
graphql (1.7.5) lib/graphql/schema.rb:275:in `execute'
app/controllers/graphql_controller.rb:3:in `execute'
actionpack (4.2.8) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
Any pointers on what's going on here and where to start for a potential fix?
Hey, not sure if this is the right project for this but I've been trying to figure out graphql in Ruby and the big issue I can't seem to figure out is how you go from a composed graphQL asking for all sorts of associations to a single SQL query loading all those associations via SQL joins.
Do the good people of Shopify have a solution for this particular problem yet? I get that it's early days still. Would this be in scope for graphql-batch?
I want to use GraphQL::Batch to load arguments for running mutations and it's not clear to me from reading the documentation if it is safe for me to use GraphQL::Batch.batch wherever I feel the need in my application. The note in the documentation refers only to using this in unit tests.
Can you provide clarification on whether GraphQL::Batch.batch can be safely used outside the test environment?
Hey Team, what's best practices for using graphql-batch on a Rails app? Am I on the right track here?
app/graphql/loaders/record_loader.rb
is this common practice?
module Loaders
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
@model = model
end
def perform(ids)
@model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
end
add path
app/graphql/memair_schema.rb
MemairSchema = GraphQL::Schema.define do
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Batch
end
adding a new field
app/graphql/types/recommendation_type.rb
require 'loaders/record_loader'
Types::RecommendationType = GraphQL::ObjectType.define do
name 'Recommendation'
field :id, !types.ID
field :description, types.String
field :events, [Types::EventType, null: true], null: false do
argument :ids, [ID], required: true
end
def events(ids:)
RecordLoader.for(Event).load_many(ids)
end
end
Right now I'm getting: GraphQL::Define::NoDefinitionError (GraphQL::Field can't define 'null'):
But I'll keep digging if I'm on the right path :)
Hi, for a query like this:
query {
company(id: 10) {
job {
company {
id
}
}
}
I checked that (with my loaders) it will do 3 queries. First to fetch the Company, then Job, then Company again. The last query is cached by ActiveRecord, so it's not a big deal. I am wondering if it should be cached in graphql-batch instead. Both the top level company
field and company
under Job
type are essentially resolved like this: RecordLoader.for(Company).load(10)
but the caches seem to live until all direct children and siblings are resolved. In other words, the cache is effectively purged by the time my nested company
field is being resolved.
Is there a reason why we couldn't cache these loaders for longer periods of time?
Hi, would you mind releasing master to rubygems? 0.3.3 has the caching patch which was removed on master. Master is testing a bit better for but it's easier for our CI to point at rubygems. Could you cut a new version?
Following up from rmosolgo/graphql-ruby#354 (comment):
For graphql-batch I would just recommend using Promise.map_value to get the end of the field resolution:
class MyFieldInstrumentation
def instrument(type, field)
old_resolve_proc = field.resolve_proc
new_resolve_proc = ->(obj, args, ctx) {
Rails.logger.info("#{type.name}.#{field.name} START: #{Time.now.to_i}")
result = old_resolve_proc.call(obj, args, ctx)
Promise.map_value(result) do |value|
Rails.logger.info("#{type.name}.#{field.name} END: #{Time.now.to_i}")
value
end
}
field.redefine do
resolve(new_resolve_proc)
end
end
end
Actually the above does not work (or at least not in a particularly useful way). The problem is that the executor does not necessarily run the loader associated with the field immediately, instead it may wait for some other loaders (or fields I suppose) to execute first. So the "start time" logged above (when the field is added to the loader), is not really "correct", in terms of the work done to fetch the field.
As an example, if I run a query against our example GitHunt server that looks something like:
{
entries {
# this field goes to a batch loader that talks to github
postedBy
# this field goes to another github batch loader
repository
# this field goes to a simple SQL batch loader
vote
}
}
You end up seeing traces that look like:
Note in the above that the start time of all the fields is more or less the same (as the executor runs over the set of entries, and they are all added to the various loaders), and the total time is "cumulative". In actuality the vote
loader is more or less instantaneous (running against a local sqlite db in this case), and a correct start time for those fields should really be at the end of the repository
loader (so I guess 2.03ms in this SS).
This is why I think for proper instrumentation of the batch loader I think we need to know two things:
The first part is trivial I suppose, but the second seems tricky.
How would I go about define a custom loader within the GraphQL field definition, instead of creating a loader class?
Can graphql-batch be used to load associations with scopes?
I was hoping updating the preload_association method in the AssociationLoader example might do the job like so:
def preload_association(records)
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name, @scope)
end
However this doesn't appear to work and reverts to n+1 queries when used in graphql. It does work when used outside graphql in a rails console strangely enough. e.g.
ActiveRecord::Associations::Preloader.new.preload( Place.all, :opening_times, OpeningTime.where(x: 'y') )
Using the latest graphql gem with latest graphql-batch gem yields errors. #101
NoMethodError: undefined method `extension' for nil:NilClass
0.4.0 is behind master for months since Feb 22, 2019 #103
Diff between master and latest release https://github.com/Shopify/graphql-batch/compare/v0.4.0...master?expand=1
Right now we have resorted to pointing ref to latest commit hash on master in our Gemfile, seems to work for now, but are there any plans to release a new gem version like 0.4.1?
In Rails there is the model.destroy(id)
command to delete (with callbacks) a model. Good.
But when you use dependent: :destroy
with has_many
or has_one
it has fun never ending with N+1 queries!!! :(
Especially if I use polymorhpic associations.
See this:
Started POST "/api/v1" for 172.18.0.1 at 2017-12-28 00:05:38 +0000
Processing by GraphqlController#execute as */*
Parameters: {"query"=>"mutation deleteProductMutation {\n deleteProduct(id: 1) {\n id\n }\n}\n", "graphql"=>{"query"=>"mutation deleteProductMutation {\n deleteProduct(id: 1) {\n id\n }\n}\n"}}
User Load (1.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Product Load (0.4ms) SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.3ms) BEGIN
Team Load (0.4ms) SELECT "teams".* FROM "teams" WHERE "teams"."product_id" = $1 LIMIT $2 [["product_id", 1], ["LIMIT", 1]]
MotherItem Load (0.5ms) SELECT "game_items".* FROM "game_items" WHERE "game_items"."team_id" = $1 [["team_id", 1]]
PlayerProfile Load (1.3ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 1], ["profileable_type", "MotherItem"], ["LIMIT", 1]]
SQL (0.5ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 1]]
SQL (0.3ms) DELETE FROM "game_items" WHERE "game_items"."id" = $1 [["id", 1]]
PlayerProfile Load (0.5ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 2], ["profileable_type", "MotherItem"], ["LIMIT", 1]]
SQL (0.6ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 2]]
SQL (0.8ms) DELETE FROM "game_items" WHERE "game_items"."id" = $1 [["id", 2]]
PlayerProfile Load (0.6ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 3], ["profileable_type", "MotherItem"], ["LIMIT", 1]]
SQL (0.5ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 3]]
SQL (0.3ms) DELETE FROM "game_items" WHERE "game_items"."id" = $1 [["id", 3]]
PlayerItem Load (0.5ms) SELECT "sister_items".* FROM "sister_items" WHERE "sister_items"."team_id" = $1 [["team_id", 1]]
PlayerProfile Load (0.5ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 1], ["profileable_type", "PlayerItem"], ["LIMIT", 1]]
SQL (0.4ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 4]]
SQL (0.4ms) DELETE FROM "sister_items" WHERE "sister_items"."id" = $1 [["id", 1]]
PlayerProfile Load (0.6ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 2], ["profileable_type", "PlayerItem"], ["LIMIT", 1]]
SQL (0.5ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 5]]
SQL (0.7ms) DELETE FROM "sister_items" WHERE "sister_items"."id" = $1 [["id", 2]]
PlayerProfile Load (0.4ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 3], ["profileable_type", "PlayerItem"], ["LIMIT", 1]]
SQL (0.3ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 6]]
SQL (0.3ms) DELETE FROM "sister_items" WHERE "sister_items"."id" = $1 [["id", 3]]
PlayerProfile Load (0.4ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 4], ["profileable_type", "PlayerItem"], ["LIMIT", 1]]
SQL (1.4ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 7]]
SQL (0.5ms) DELETE FROM "sister_items" WHERE "sister_items"."id" = $1 [["id", 4]]
PlayerProfile Load (0.5ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 5], ["profileable_type", "PlayerItem"], ["LIMIT", 1]]
SQL (0.5ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 8]]
SQL (0.5ms) DELETE FROM "sister_items" WHERE "sister_items"."id" = $1 [["id", 5]]
PlayerProfile Load (0.4ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 6], ["profileable_type", "PlayerItem"], ["LIMIT", 1]]
SQL (1.3ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 9]]
SQL (1.9ms) DELETE FROM "sister_items" WHERE "sister_items"."id" = $1 [["id", 6]]
FootballItem Load (1.4ms) SELECT "football_items".* FROM "football_items" WHERE "football_items"."team_id" = $1 [["team_id", 1]]
PlayerProfile Load (1.0ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 1], ["profileable_type", "FootballItem"], ["LIMIT", 1]]
SQL (0.6ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 10]]
SQL (1.8ms) DELETE FROM "football_items" WHERE "football_items"."id" = $1 [["id", 1]]
PlayerProfile Load (0.3ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 2], ["profileable_type", "FootballItem"], ["LIMIT", 1]]
SQL (1.0ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 11]]
SQL (0.4ms) DELETE FROM "football_items" WHERE "football_items"."id" = $1 [["id", 2]]
PlayerProfile Load (0.4ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 3], ["profileable_type", "FootballItem"], ["LIMIT", 1]]
SQL (0.6ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 12]]
SQL (0.6ms) DELETE FROM "football_items" WHERE "football_items"."id" = $1 [["id", 3]]
PlayerProfile Load (0.9ms) SELECT "player_profiles".* FROM "player_profiles" WHERE "player_profiles"."profileable_id" = $1 AND "player_profiles"."profileable_type" = $2 LIMIT $3 [["profileable_id", 4], ["profileable_type", "FootballItem"], ["LIMIT", 1]]
SQL (0.6ms) DELETE FROM "player_profiles" WHERE "player_profiles"."id" = $1 [["id", 13]]
SQL (0.3ms) DELETE FROM "football_items" WHERE "football_items"."id" = $1 [["id", 4]]
SQL (2.7ms) DELETE FROM "teams" WHERE "teams"."id" = $1 [["id", 1]]
SQL (2.6ms) DELETE FROM "products" WHERE "products"."id" = $1 [["id", 1]]
(3.3ms) COMMIT
Completed 200 OK in 522ms (Views: 0.4ms | ActiveRecord: 80.5ms)
user: root
POST /api/v1
USE eager loading detected
MotherItem => [:player_profile]
Add to your finder: :includes => [:player_profile]
Call stack
/app/graphql/delete.rb:15:in `call'
/app/controllers/graphql_controller.rb:11:in `execute'
user: root
POST /api/v1
USE eager loading detected
PlayerItem => [:player_profile]
Add to your finder: :includes => [:player_profile]
Call stack
/app/graphql/delete.rb:15:in `call'
/app/controllers/graphql_controller.rb:11:in `execute'
user: root
POST /api/v1
USE eager loading detected
FootballItem => [:player_profile]
Add to your finder: :includes => [:player_profile]
Call stack
/app/graphql/delete.rb:15:in `call'
/app/controllers/graphql_controller.rb:11:in `execute'
As you can see gem Bullet (https://github.com/flyerhzm/bullet) detects N+1 problems...
Is there a way to handle this? Maybe with the same gem for batching and preloader? (https://github.com/Shopify/graphql-batch) - (https://github.com/ConsultingMD/graphql-preload) ?
Looks like the interface for connections has changed somehow
NoMethodError - undefined method `offset' for #<Team:0x00000007fc2d60>:
activemodel (4.2.6) lib/active_model/attribute_methods.rb:433:in `method_missing'
graphql-relay (0.9.5) lib/graphql/relay/relation_connection.rb:32:in `sliced_nodes'
graphql-relay (0.9.5) lib/graphql/relay/relation_connection.rb:23:in `paged_nodes'
graphql-relay (0.9.5) lib/graphql/relay/relation_connection.rb:82:in `paged_nodes_array'
graphql-relay (0.9.5) lib/graphql/relay/relation_connection.rb:5:in `cursor_from_node'
graphql-relay (0.9.5) lib/graphql/relay/edge.rb:14:in `cursor'
graphql (0.13.0) lib/graphql/field.rb:111:in `block in build_default_resolver'
graphql (0.13.0) lib/graphql/field.rb:67:in `resolve'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:69:in `block in get_middleware_proc_from_field_resolve'
graphql (0.13.0) lib/graphql/schema/middleware_chain.rb:24:in `call'
graphql (0.13.0) lib/graphql/schema/rescue_middleware.rb:34:in `call'
graphql (0.13.0) lib/graphql/schema/middleware_chain.rb:24:in `call'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:58:in `get_raw_value'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:26:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:98:in `resolve_field'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:18:in `block in result'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:17:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/value_resolution.rb:72:in `non_null_result'
graphql (0.13.0) lib/graphql/query/serial_execution/value_resolution.rb:23:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:45:in `get_finished_value'
() home/andrew/bundle/ruby/2.3.0/bundler/gems/graphql-batch-9cf8341196a8/lib/graphql/batch/execution_strategy.rb:56:in `get_finished_value'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:30:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:98:in `resolve_field'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:18:in `block in result'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:17:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/value_resolution.rb:72:in `non_null_result'
graphql (0.13.0) lib/graphql/query/serial_execution/value_resolution.rb:23:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:45:in `get_finished_value'
() home/andrew/bundle/ruby/2.3.0/bundler/gems/graphql-batch-9cf8341196a8/lib/graphql/batch/execution_strategy.rb:56:in `get_finished_value'
graphql (0.13.0) lib/graphql/query/serial_execution/field_resolution.rb:30:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:98:in `resolve_field'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:18:in `block in result'
graphql (0.13.0) lib/graphql/query/serial_execution/selection_resolution.rb:17:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution/operation_resolution.rb:20:in `result'
graphql (0.13.0) lib/graphql/query/serial_execution.rb:17:in `execute'
() home/andrew/bundle/ruby/2.3.0/bundler/gems/graphql-batch-9cf8341196a8/lib/graphql/batch/execution_strategy.rb:4:in `execute'
graphql (0.13.0) lib/graphql/query/executor.rb:38:in `execute'
graphql (0.13.0) lib/graphql/query/executor.rb:15:in `result'
graphql (0.13.0) lib/graphql/query.rb:47:in `result'
graphql (0.13.0) lib/graphql/schema.rb:55:in `execute'
app/controllers/api/v1/graph_ql_controller.rb:14:in `execute'
I currently have graphql-ruby acting as a front end for our LDAP directory. graphql-batch works really great against directory data. I've even gone so far as to cache the ldap query results that are formed by the batch loaders using Rails.cache statement.
But I'd like the caching to be a bit more granular (i.e. cache individual objects, and not entire sets of objects returned by batch queries.
The problem is when I try to wrap an ObjectLoader.for statement in Rails.cache.fetch block I get an
TypeError (no _dump_data is defined for class Proc):
error.
This seems be because Rails.cache can't cache a promise. Any thoughts or approaches on how to cache the results of an individual fulfill from a batch loader?
Heres some sample code from one of my resolvers that causes the above error:
def call(obj, args, ctx)
# calculate environment from the top level env ctx
myEnv = get_root_query_env(ctx,args)
# lets setup a cache key for this LDAP query
myCacheKey = "ldap/#{myEnv}/#{args[:dn]}/#{Base64.encode64(LDAP::LDAP_USER_READABLE_ATTRS.to_s)}"
Rails.logger.debug "Checking Rails Cache for key for LDAP User DN: #{args[:dn]}"
# get cached result or if not cached, do actual query
Rails.cache.fetch(myCacheKey, expires_in: LDAP::LDAP_GLBL_CACHE_EXPIRE_MINS.minutes) do
# send the request for this user DN to the batch loader
get_user_from_batch(myEnv, args[:dn])
end
end
private
def get_user_from_batch( myEnv, dn)
# Call Generic directory Object loader so that this query can be batched
Dir::Loaders::ObjectLoader.for("ldap", myEnv, "user").load(dn).then(nil, lambda do |exc|
# looks like this query errored or wasn't found
Rails.logger.error exc
GraphQL::ExecutionError.new("#{exc}: Object Not Found")
end ) do |thisObj|
# return the resolved object from the batch loader
thisObj
end
end
The README shows how to integrate this gem with your project by calling use
from within a GraphQL::Schema
subclass:
class MySchema < GraphQL::Schema
query MyQueryType
mutation MyMutationType
use GraphQL::Batch
end
However, for our project, we do not have a schema class; instead we are loading our schema at boot time from a .graphql
file:
schema = GraphQL::Schema.from_definition(graphql_schema_string, default_resolve: ->(*args) { MyResolver.resolve(*args) })
I did some digging through the source code of the graphql
gem and this gem, and found that I could integrate this gem by doing the following:
GraphQL::Define::DefinedObjectProxy.new(schema).use(GraphQL::Batch)
GraphQL::Define::DefinedObjectProxy
is the only way I could figure out to get this to work, given GraphQL::Batch#use
calls target
, which, as afar as I can tell, is only defined on GraphQL::Define::DefinedObjectProxy
. This works, but took a lot of digging, and it feels like I'm using a private API as I don't think the author of the graphql gem intends DefinedObjectProxy
to be something that end-users directly instantiate like this.
Am I missing a simpler way to integrate graphql-batch with my project? Or is there a simple change that could be made to graphql-batch
to support GraphQL::Batch.use(schema)
or something similar?
I'm trying to use this with a resolver class, e.g:
class EntityResolver < GraphQL::Schema::Resolver
argument :id, ::GraphQL::Types::ID, required: true
def resolve(id:)
::Loaders::RecordLoader.for(Entity).load(id)
end
end
In my Schema
field "entity", EntityType, null: false, resolver: EntityResolver
And do query, then I got
`GraphQL::Batch::NoExecutorError - Cannot create loader without an Executor.
Wrap the call to `for` with `GraphQL::Batch.batch`
or use `GraphQL::Batch::Setup` as a query instrumenter if using with `graphql-ruby`
I've tried GraphQL::Batch.batch { ::Loaders::RecordLoader.for(Entity).load(id) }
but got GQL's null error message.
How to working with resolver?
After executing a perform
to resolve the promises into records, I'd like to be able to add "Record not found" errors for unfound ids to the graphql-ruby error array. However, I don't see a way to do that from with in RecordLoader#perform
, or after. Is there a way to access the query context from this method? Or any other mechanism for this?
Thanks ...
Does this project have a changelog? I want to update from 0.3.3 to 0.3.9 and I just want to make sure nothing breaks.
Thanks!
[This is not a bug, but I don't know where else to put this]
I am trying to wrap my head around what I think is a pretty common use case, and am hoping there's some prior art from others I can learn from. The high level summary is that, given the following query:
query fetchProperty {
property(id: 1) {
id
name
}
property(id: 2) {
id
name
}
}
I want my batch loader to generate the SQL SELECT id, name FROM properties WHERE id IN (1, 2)
. Now I have built a loader to generate SELECT * FROM properties WHERE id IN (1, 2)
without issues, but I am struggling to understand if / how its possible to have the sub-fields of the field requested affect the loader's strategy.
Relevant code for query:
module GraphType
QueryType = GraphQL::ObjectType.define do
field :property, -> { PropertyType } do
argument :id, !types.ID
resolve -> (obj, args, ctx) {
# this is a simple ActiveRecord batch loader that aggregates IDs and
# fetches them with `.where(id: ids)`
GraphTool::FindLoader.for(Property.all).load(args[:id])
}
end
end
end
module GraphType
PropertyType = GraphQL::ObjectType.define do
field :id, !types.ID
field :name, !types.String
field :address, !types.String
field :email, !types.String
end
end
I guess what I'm asking is, is there a context available where the property
promise has not yet been resolved, but property.name
's resolve function is being executed?
I was trying to reuse a loader, but if the second load provides a different input id it will crash with GraphQL::Batch::BrokenPromiseError: Promise wasn't fulfilled after all queries were loaded
.
Example:
loader = SomeLoader.for
loader.load(1).sync # First load
loader.load(1).sync # Everything's okay. Record already loaded.
loader.load(2).sync # Crashes.
# gems/promise.rb-0.7.1/lib/promise.rb:84:in `block in reject'
# gems/promise.rb-0.7.1/lib/promise.rb:114:in `dispatch'
# gems/promise.rb-0.7.1/lib/promise.rb:82:in `reject'
# gems/graphql-batch-0.2.4/lib/graphql/batch/executor.rb:33:in `wait'
# gems/graphql-batch-0.2.4/lib/graphql/batch/promise.rb:4:in `wait'
# gems/promise.rb-0.7.1/lib/promise.rb:64:in `sync'
It crashes before reaching the #perform
method at all.
Is this by design? I think the README should mention it in that case.
Is there an public API documentation for the GraphQL::Batch::Loader
interface? It would be nice to link from the readme to explain e.g. what on earth cache_key
, fulfill
, mean.
In addition, it would be nice to introduce the user to the moving parts of RecordLoader
and AssociationLoader
--the single comment in https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb raises far more questions than it answers.
Thanks!
What's the recommended way to paginate a response when using AssociationLoader
?
I've tried the following:
field :posts_connection, Types::PostType.connection_type, null: false
def posts_connection
AssociationLoader.for(model: User, association_name: :posts).load(object)
end
but this results in two SQL queries being generated, one with my relay-defined pagination queries and one without.
I read through #26 and rmosolgo/graphql-ruby#425 but am unclear on how to use the lazy execution API to solve this. Adding the following to my schema did not help:
lazy_resolve(AssociationLoader, :perform)
Opening this issue to gauge interest in configuring rubocop for this project. I'd be happy to work on a PR to support this, but given the level of effort wanted to confirm the maintainers interest in introducing linting.
Sample PR to demo some rules that might be applied by default + integration in CI / gemspec (does not contain all fixes): #118
Could there be a Sequel association_loader.rb
example , just like the Active Record one?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.