Improvement to the use of Node.ID and additional information on GraphQL
ivanchoo opened this issue · 2 comments
Thank you @mbrochh for the introduction to the GraphQL stack at the Djangonauts meetup. Kudos to the well prepared presentation and sample codes.
I have a few thoughts and suggestions which I didn't raise during the meetup due to time constrain, hence I'm sharing it here for your consideration. (I'm relatively new to GraphQL so please take my words with a pinch of salt.)
Node
It may be worth mentioning that Node
is part of the Facebook Relay specs (not GraphQL specs). However, most framework (Graphene included) implement this spec due to the close association between Relay and GraphQL.
Essentially Node
is an interface that implements just an ID
field, which is meant to be an globally unique identifier for an object. ID
s are designed to be opaque (typical convention is to base64('type:id')
), applications should not attempt to rely on this implementation detail.
Node
is exposed as part of the root Query, where applications can query against entities with known ID
in a convenient way, e.g. refetching, fetching fields that have not been fetched.
{
node(id: ID!) {
... on User {
id
userId
name
}
... on Company {
id
companyId
owner: {
userId
name
}
}
...
}
}
This provides the convenience of not having to define query point for every model you expose (e.g. message(messageId)
or user(userId)
). We make our schema models inherit the Node interface to take advantage of this feature.
# Based on graphene-sqlalchemy, but should work the same for graphene-django
class Query(graphene.ObjectType):
# Expose `node` as part of root query
# Framework will extract `ID` and instantiate the object
# base on the type and primary key information
node = relay.Node.Field()
class User(SQLAlchemyObjectType):
class Meta:
model = UserModel
# Implements the `Node` interface.
# Framework is smart enough to generate the `ID` value
# base on the database object's primary key.
interfaces = (relay.Node, )
One common gotcha is people often confuse ID
with the object's identifier (e.g. primary key of the database object). They are not the same thing! Node.ID
is a Relay/GraphQL implementation detail, and should not be meaningful in your application domain. My recommendation is to expose both ID
and your object id in your schema. This gets tricky when your ORM primary key is named id
, in which case you'll probably need to write a custom resolver.
class User(SQLAlchemyObjectType):
class Meta:
model = UserModel
interfaces = (relay.Node, )
local_fields = {
'user_id': Field(Int)
}
def resolve_user_id(self, args, context, info):
# self points to the ORM object
return self.id
This way your application can use the object id in a meaningful way, e.g in router's URL path, locating external resources, printing our reference numbers etc.
Connection
Like Node
, Connection
is also part of the Relay specs that made its way to mainstream adoption.
At first glance, the concept of edges
seems superfluous but it does solve some tricky use case. Consider the need to expose a many-to-many relationship like 'friends', typically implemented in a database with a join table.
+---------+ +------------+
| users | | friends |
+---------+ +------------+
| user_id | <------ | left_id |
| .... | \--- | right_id |
+---------+ | created_at |
+------------+
It is now easy to display "Friends since [date here]" by exposing friends.created_at
in the edge object.
{
user {
friends {
edges {
created_at <---
user {
id
userId
name
}
}
}
}
}
BTW, cursor
is part of the Connection
specs which is exposed as a field on edge
.
{
connection {
edges {
cursor <- cursor is unique for each edge
node
}
pageInfo {
...
lastCursor <- points to the cursor in the last edge
}
}
}
I hope you can find these information useful.
Oh my god, this is awesome!! In my own code I am actually using a CustomNode
implementation that hides away the Relay-style ID and allows me to use Django's PKs, but I didn't want to show that in the talk because it would only make things even more complicated.
I have to read this a few more times and finally understand Nodes/Edges/IDs better and will try to change the code accordingly. I hope local_fields
is also possible on the DjangoObjectType
, exposing the PK that way would be awesome.
I hope local_fields is also possible on the DjangoObjectType, exposing the PK that way would be awesome.
AFAIK, graphene-django supports local_fields
in the Meta
as well (should work the same). Also checkout only_fields
and exclude_fields
for hiding ORM fields in schema.
See DjangoObjectTypeMeta.
The graphene framework is quite well thought out, but documentation is unfortunately lacking.