For the sake of my own sanity, everything I developed as in Graphene, so whenever I mention GraphQL please substitute with “GraphQL/Graphene” where it makes sense
GraphQL is an interesting approach to graph queries (after all it stands for Graph Query Language), as it does not explicitly sit on a graph database. Rather it seems to make use of various data loaders and constructors in order to create a graph-like experience. In this fashion GraphQL can feel like a virtual database; but for graph outputs.
That being said, it isn’t always obvious how to design a simple API for it, but it becomes easier if we stick to a few key principals GraphQL appears to adhere to:
Key-values in everything!
Do you need to write a DataLoader
? Please think in terms of key-value pair. Do you need to execute a query
? Key-value pair.
The relevant aspects to this is that the key
can be an abstract type, such as a String
or a Float
, a [String]
or even another object. Similar for the value
. This makes it really powerful and at the same time confusing if that construct wasn’t in your mind.
For example, consider Getting Started page on graphene.
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return 'Hello ' + name
schema = graphene.Schema(query=Query)
result = schema.execute('{ hello }')
print(result.data['hello']) # "Hello stranger"
If I wanted to change it to, return firstname + lastname
would I do:
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(firstname=graphene.String(default_value="stranger"), lastname=graphene.String(default_value="doe"))
def resolve_hello(self, info, firstname, lastname):
return 'Hello ' + firstname + " " + lastname
schema = graphene.Schema(query=Query)
result = schema.execute('{ hello }')
print(result.data['hello'])
It would appear to work! But how would I substitute variables into it? One approach might look like this:
import graphene
class HelloInfo(graphene.ObjectType):
hello = graphene.String()
class Query(graphene.ObjectType):
hello = graphene.Field(HelloInfo, firstname=graphene.String(default_value="stranger"))
def resolve_hello(self, info, firstname, lastname="doe"):
return HelloInfo('Hello ' + firstname + " " + lastname)
schema = graphene.Schema(Query)
result = schema.execute(
'''query hello($firstname: String) {
hello(firstname: $firstname) {
hello
}
}''',
variable_values={'firstname': "John"},
)
if result.errors:
print(result.errors)
print(result.data['hello'])
How about adding back the lastname
variable?
import graphene
class HelloInfo(graphene.ObjectType):
hello = graphene.String()
class Query(graphene.ObjectType):
hello = graphene.Field(HelloInfo, firstname=graphene.String(default_value="stranger"), lastname=graphene.String(default_value="doe"))
def resolve_hello(self, info, firstname, lastname):
return HelloInfo('Hello ' + firstname + " " + lastname)
schema = graphene.Schema(Query)
result = schema.execute(
'''query hello($firstname: String) {
hello(firstname: $firstname) {
hello
}
}''',
variable_values={'firstname': "John", 'lastname': "strange"},
)
if result.errors:
print(result.errors)
print(result.data['hello']) # 'Hello John doe'???
It doesn’t return the provided lastname variable item, nor does it return an error! What happend here?
It turns out we have the wrong understanding of what the arguments do, instead we should write it up as follows:
import graphene
class HelloInfo(graphene.ObjectType):
hello = graphene.String()
class Query(graphene.ObjectType):
hello = graphene.Field(HelloInfo, name=graphene.List(graphene.String))
def resolve_hello(self, info, name):
full_name = name[0] + " " + name[1]
return HelloInfo('Hello ' + full_name)
schema = graphene.Schema(Query)
result = schema.execute(
'''query hello($name: [String]) {
hello(name: $name) {
hello
}
}''',
variable_values={'name': ["John", "Strange"]},
)
if result.errors:
print(result.errors)
print(result.data['hello'])
Which is covered here. Similarly for DataLoader, the idea applies. When you write the batch_load_fn
the keys
can be an item like [String]
, and the resolution can be an arbitary object. In this way it enforces particular typing so that you can built complex queries.