Upgrade to Pro — share decks privately, control downloads, hide ads and more …

An Introduction to Graphene and Relay

52654cfef40a0579cb31843b01ba49bc?s=47 Marc Tamlyn
September 19, 2016

An Introduction to Graphene and Relay

Workshop slides for an introduction to Grahene and Relay. Associated code base can be found at github.com/mjtamlyn/graphene-tutorial/. I would like to see this material improved over time and turn into a full set of documentation for graphene/graphene-django.

52654cfef40a0579cb31843b01ba49bc?s=128

Marc Tamlyn

September 19, 2016
Tweet

Transcript

  1. An introduction to Graphene and Relay

  2. Part 0: Introduction

  3. Who am I? • Marc Tamlyn • Django Core Developer

    • Graphene contributor • Occasional Javascript developer • Work at Photocrowd
  4. Workshop structure • Split into four main parts • GraphQL

    • Graphene • React • Relay
  5. What is all this? • GraphQL is a new API

    structure created by Facebook • Graphene is a declarative python library for creating GraphQL APIs by the python-graphql organisation • It builds on top of graphql-core, a python version of the "official" graphql-js library • React is a declarative, component based Javascript rendering engine created by Facebook • Relay is a React-drive framework for consuming a GraphQL API and building a highly flexible and reusable component based application architecture
  6. What's the point? • Allow your front ends to declare

    the shape of the data they require • Move n+1 problems to the API server instead of the front end • Components declare their own requirements and can be composed, allowing changes deep in the system to work everywhere • Great for applications where the same objects are represented and connected in many different ways
  7. A word on investment • Personally, I find these concepts

    are relatively "hard" compared to understanding a REST API • It takes a higher investment of effort to understand all the pieces and how they all fit together than some other architectures • It requires more work and rigour to code using these libraries • Personally, I think the effort pays off • Give it a week!
  8. Data setup • A little (fictional!) welsh village • 3

    streets, 10 houses • People live in the houses and are related to each other • People can get married, have children and move house • Application should allow you to explore the village and find out about the people who live there, and update the data as they move, marry, have children and so on
  9. Part 1: GraphQL

  10. GraphQL • New query language • Strongly typed • Returns

    JSON
  11. Work along with me https://graphene-tutorial.herokuapp.com/plain/ https://github.com/mjtamlyn/graphene-tutorial/

  12. Type system • Everything consists of fields on an object

    • Useful to know what fields are available, and what type of data to expect • Allows static validation of queries
  13. Scalars • Int • Float • String • Boolean •

    ID • Can define your own - needs to be JSON serialisable
  14. Lists and nulls • List of any other type •

    Fields are nullable unless specified as non-null
  15. Enumerations types • First class support for enums • Collection

    of unique string names • Allows query-time validation of arguments
  16. Type system • Object type • Consists of fields •

    Each field returns data of a specified type • Data can be any type, including other object types • Each field may take arguments • Each argument also has a fixed type, and may or may not be nullable
  17. Interfaces and Unions • Object types can be combined in

    a union • They can implement one or more interfaces, keeping a common set of behaviour between multiple types
  18. Schema • Schema is an ObjectType • Must have a

    query field • May have a mutation field
  19. Queries • Written in GraphQL query language • Returns JSON

  20. A simple query query { streets { name } }

    {"data": { "streets": [ {"name": "Afon Stryd"}, {"name": "Glyn Stryd"}, {"name": "Ysgol Stryd"} ] }}
  21. Nesting query { streets { name houses { number }

    } } {"data": { "streets": [ {"name": "Afon Stryd", "houses": [ {"number": 1}, {"number": 2}, {"number": 3}, ]}, …
  22. Multiple queries query { streets { name } houses {

    name } } {"data": { "streets": [ {"name": "Afon Stryd" … ] "houses": [ {"name": "1 Afon Stryd"} …
  23. Arguments and variables query { people { name family relationships(relationship:

    child) { name family } } }
  24. Arguments and variables query People($relationship: Relationship!) { people { name

    family relationships(relationship: $relationship) { name family } } } variables {"relationship": "child"}
  25. Arguments and variables {"data": { "people": [ {"name": "Lowri", "family":

    "Jones", "relationships": [ {"name": "Dafydd", "family": "Jones"}, {"name": "Bethan", "family": "Jones"}] …
  26. Fragments and aliases fragment F0 on Person { name residence

    { name } }
  27. Fragments and aliases query { people { name family children:

    relationships(relationship: child) {...F0} parents: relationships(relationship: parent) {...F0} } }
  28. Fragments and aliases "people": [ {"name": "Lowri", "family": "Jones", "children":

    [ {"name": "Dafydd", "residence": { "name": "1 Glyn Stryd"} }, "parents": []}, …
  29. Inline fragments { search(q: "ysgol") { __typename ... on Street

    { name } ... on House { street { name } number } } }
  30. Inline fragments "search": [ {"__typename": "House", "street": { "name": "Ysgol

    Stryd" }, "number": 1 }, {"__typename": "Street", "name": "Ysgol Stryd" }, … ]
  31. Directives query People($includeChildren: Boolean!) { people { name family relationships(relationship:

    child) @include (if: $includeChildren) { name } } } variables {"includeChildren": false}
  32. Directives { "data": { "people": [ { "name": "Lowri", "family":

    "Jones" }, … }
  33. Have an explore! https://graphene-tutorial.herokuapp.com/plain/

  34. Mutations • The only way to change data with a

    GraphQL API • Looks like a field - takes arguments and returns an ObjectType • Run in series instead of in parallel
  35. Input type • Can define a special kind of type

    called and input type • Allows you to send the JSON representation of that type as a variable value
  36. Making a mutation mutation Move($input: MoveInput!) { move(input: $input) {

    name residence { name } } } variables {"input": {"person": "ID1", "residence": "ID2"}}
  37. Part 2: Graphene

  38. Defining an object type • Let's start with something really

    simple • Street has just one attribute - its name • It should be a string
  39. Defining an object type class Street(graphene.ObjectType): name = graphene.Field(graphene.String) And

    you're done!
  40. Defining an object type - shorthand class Street(graphene.ObjectType): name =

    graphene.String()
  41. Object types as containers street = Street(name='Araf Stryd')

  42. A note on naming • GraphQL expects to have camelCase

    names • Python doesn't like camelCase names • Graphene just translates them for you - so an object type field named house_pets becomes a GraphQL field named housePets
  43. Custom initialisation and resolvers • You will likely want to

    translate from your application objects to ObjectTypes • Some fields you want to expose you don't know yet • Custom resolver method using the resolve_FOO() pattern • Resolvers receive the field arguments, your own context and the current resolver info
  44. Custom initialisation and resolvers class Street(graphene.ObjectType): name = graphene.String() def

    __init__(self, street): self.street = street def resolve_name(self, args, ctx, info): return self.street.name
  45. Custom initialisation and resolvers class Street(graphene.ObjectType): houses = graphene.List(House) def

    resolve_houses(self, args, context, info): return [ House(h) for h in population.houses if h.street == self.street ]
  46. Custom fields • Fields have their own resolver method •

    Allows you to share common custom resolving logic between multiple object types
  47. Defining an enum • Declarative • Uses python 3's enum.Enum

    (or a backport)
  48. Defining an enum class Relationship(graphene.Enum): parent = 'parent' grandparent =

    'grandparent' child = 'child' grandchild = 'grandchild' spouse = 'spouse' sibling = 'sibling'
  49. Defining a field with arguments • Allows fields to behave

    differently depending on what the client needs • Examples would include pagination, image sizes, filtering a dataset
  50. Defining a field with arguments class Person(graphene.ObjectType): relationships = graphene.Field(

    graphene.List(lambda: Person), relationship=graphene.Argument( Relationship, required=True, ), )
  51. Defining a field with arguments class Person(graphene.ObjectType): relationships = graphene.List(

    lambda: Person, relationship=graphene.Argument( Relationship, required=True, ), )
  52. Defining a field with arguments class Person(graphene.ObjectType): def resolve_relationships( self,

    args, context, info ): if args['relationship'] == 'parent': return [ Person(p) for p in self.person.parents ] …
  53. Union Type • Allows returning one of a variety of

    types • Example use cases include search endpoints, generic relationships, mixed content feeds • Resolver returns any of the valid objects and graphene does the rest
  54. Union Type class Anything(graphene.types.union.Union): class Meta: types = (Person, House,

    Street)
  55. Creating your schema • Create a Query ObjectType • Plumb

    that into the schema
  56. Creating your schema class Query(graphene.ObjectType): people = graphene.List(Person) houses =

    graphene.List(House) streets = graphene.List(Street) search = graphene.Field( graphene.List(Anything), q=graphene.String(required=True) ) )
  57. Creating your schema schema = graphene.Schema(query=Query) schema_with_mutations = graphene.Schema( query=Query,

    mutation=Mutation, )
  58. Using the schema directly >>> schema.execute(''' query { streets {

    name } } ''') {"data": {"streets": […]}}
  59. Context and info • When you use the schema, you

    can provide context • This is useful for attaching authentication or authorisation information you wish to use in resolvers • Info provides information about the current set of fields "below" where you are which are requested • Advanced use case, but can use to optimise the way you load data for the children
  60. Mutations • Must define its Input type • Normal class

    definition is the object type returned • Has a mutate classmethod which does the work if the input is valid
  61. Mutations class MoveHouse(graphene.Mutation): class Input: person = graphene.Field(Person) house =

    graphene.Field(House) # output person = graphene.Field(Person) ok = graphene.Boolean()
  62. Mutations class MoveHouse(graphene.Mutation): @classmethod def mutate(cls, instance, args, info): person

    = get_person(args['person']) house = get_house(args['house']) ok = person.move_to(house) return MoveHouse(person=person, ok=ok)
  63. Deploying with Django • Use the graphene-django package • The

    django-graphiql package allows interactive testing - add it to INSTALLED_APPS • Integration tools exist for other systems like SQLAlchemy
  64. Deploying with Django from django.conf.urls import url from django.views.decorators.csrf import

    csrf_exempt from graphene_django.views import GraphQLView from django_graphiql.views import GraphiQL from .api import Schema urlpatterns = [ url(r'^graphql', csrf_exempt( GraphQLView.as_view(schema=schema))), url(r'^graphiql', GraphiQL.as_view()), ]
  65. Part 3: React

  66. What is React? • View layer of a traditional MVC

    • Virtual DOM • Single directional data flow
  67. What is a component? • Takes input (props) • Returns

    an element • Class with a render() method
  68. What is a component? var houses = React.createClass({ render: function()

    { var photos = this.props.houses.map((house) => { return React.createElement(House, { number: house.number, streetName: house.street.name, }); }); return React.createElement('div', {}, houses); } });
  69. JSX • Preprocessor for React.createElement() • Compile using your JS

    preprocessor of choice
  70. JSX var houses = React.createClass({ render: function() { var photos

    = this.props.houses.map((house) => { return <House number=house.number streetName=house.street.name >; }); return <div>{ houses }</div>; } });
  71. Virtual DOM • Renders in memory-light virtual DOM • Compares

    changes between trees • Flushes only changes to the document • Debounces rendering
  72. Data flow • Components have state as well as props

    • Minimise use of state • Handle at top level and pass functions around • Flux-like architectures • Reflux • Redux • Use Relay!
  73. Part 4: Relay

  74. Why Relay? • Only GraphQL-focused React framework • Colocation •

    Data masking • Caching • Query optimisation • Loading and ready states • Don't want something this restrictive? Try apollo-client
  75. A Relay-compliant GraphQL API • Relay imposes conventions on the

    shape of your API • Caches and optimises queries through nodes • Handles pagination intelligently via cursors and connections
  76. Nodes • All "core" objects should be implemented as Nodes

    • If there would be a "detail" page of it, it should be a node • All nodes have a globally unique ID • Query has a node field which can query any node
  77. Nodes class StreetNode(DjangoObjectType): class Meta: model = Street only_fields =

    ['name'] interfaces = (relay.Node,)
  78. Nodes query { streets { id name } } {"data":

    { "streets": [ {"id": "XXXXXX", "name": "Afon Stryd"}, {"id": "YYYYYY",
 "name": "Glyn Stryd"}, {"id": "ZZZZZZ",
 "name": "Ysgol Stryd"} ] }}
  79. Nodes query { node(id: "XXXXXX") { id name } }

    {"data": { "node": { "id": "XXXXXX", "name": "Afon Stryd", } }}
  80. Connections • Connections are special object types which have a

    particular structure • Used to do cursor based pagination • first - after • last - before • Return edges and pageInfo • Edges have cursors and nodes
  81. Connections class StreetNode(DjangoObjectType): houses = relay.ConnectionField( HouseNode) def resolve_houses(self, args,

    ctx, info): return self.house_set.all()
  82. Connections query { streets { name houses(first: 1) { edges

    { cursor node { number } } } } }
  83. Connections {"data": {"streets" { "name": "Afon Stryd", "houses": { "edges":

    [{ "cursor": "AAAAAA", "node": { "number": 1, } }, …
  84. Connections query { streets { name houses(first: 1) { pageInfo

    { hasNextPage endCursor } } } }
  85. Connections {"data": {"streets" { "name": "Afon Stryd", "houses": { "pageInfo":

    { "hasNextPage": true, "endCursor": "AAAAAA", } …
  86. Connections query { streets { name houses(first: 1, after: "AAAAAA")

    { … } } }
  87. Mutations • Mutations must both take and return a unique

    identifier • Allows multiple similar mutations to be checked individually by the framework
  88. Relay containers • A higher order component • Function which

    takes a component and returns a component which wraps it • Compile-time declares the data shape that the component needs
  89. Declaring fragments • Uses Relay.QL`` • A little sugar on

    top of a standard GraphQL fragment to allow reference to variables more easily • Looks a bit like ES6 template literals • Compiles using a babel plugin
  90. Composing containers • When building the fragments, include fragments from

    the child • Match the name of the prop passed to the child to the fragment that child declared
  91. Root Containers • Declares the entry point to the Relay

    application • Consists of a component to render, and a "rooted" query to the Query on the Node • Don't try to be too clever with them, they have some strange restrictions about arguments which will likely be removed in future versions
  92. The "viewer" pattern • Works around Relay Root queries being

    weird by doing everything from a viewer field on Query • Don't forget that Relay's node field still needs to be on Query as well • Idea is everything is localised to what the user can see
  93. Mutations • Must receive and return the client identifier •

    Just use relay.ClientIDMutation
  94. Babel and webpack • Use babel to translate JSX and

    Relay.QL • Webpack with Django to create "bundles" of javascript to render to the page • graphene-django creates the schema file needed to verify the Relay.QL
  95. Dumping your schema $ python manage.py graphql_schema --schema relay.api {

    "data":{ "__schema":{ "queryType":{ "name":"Query" }, "mutationType":null, "subscriptionType":null, "types":[ { "kind":"OBJECT", "name":"Query", "description":null, "fields":[ { "name":"streets", "description":null, "args":[ ], …
  96. Things to install • Babel plugins required • babel-preset-es2015 •

    babel-preset-react • babel-relay-plugin • babel-relay-plugin-loader • Django • django-webpack-loader • python manage.py graphql_schema --schema <schema> --out schema.json
  97. Webpack config var BundleTracker = require('webpack-bundle- tracker') plugins: [ new

    BundleTracker({ filename: './build/webpack-stats.json' }), ],
  98. Webpack config module: { loaders: [ { test: /\.jsx?$/, exclude:

    /node_modules/, loader: 'babel-loader', } ] }
  99. package.json config "metadata": { "graphql": { "schema": "./schema.json" } }

  100. Conclusion

  101. Conclusion • Stack of tools for building complete applications •

    A coherent and powerful API • Hurdle to learn but expressive and effective once learned • Excellent support for complex applications • Probably overkill for simple ones • Excellent responsiveness to changing visual and interactivity requirements