mbrochh/django-graphql-apollo-react-demo

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. IDs 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.