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

An Introduction to Graphene and Relay

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.

Marc Tamlyn

September 19, 2016
Tweet

More Decks by Marc Tamlyn

Other Decks in Technology

Transcript

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

    • Graphene contributor • Occasional Javascript developer • Work at Photocrowd
  2. 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
  3. 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
  4. 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!
  5. 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
  6. 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
  7. Scalars • Int • Float • String • Boolean •

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

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

    of unique string names • Allows query-time validation of arguments
  10. 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
  11. 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
  12. Schema • Schema is an ObjectType • Must have a

    query field • May have a mutation field
  13. A simple query query { streets { name } }

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

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

    name } } {"data": { "streets": [ {"name": "Afon Stryd" … ] "houses": [ {"name": "1 Afon Stryd"} …
  16. Arguments and variables query People($relationship: Relationship!) { people { name

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

    "Jones", "relationships": [ {"name": "Dafydd", "family": "Jones"}, {"name": "Bethan", "family": "Jones"}] …
  18. Fragments and aliases query { people { name family children:

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

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

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

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

    child) @include (if: $includeChildren) { name } } } variables {"includeChildren": false}
  23. 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
  24. 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
  25. Making a mutation mutation Move($input: MoveInput!) { move(input: $input) {

    name residence { name } } } variables {"input": {"person": "ID1", "residence": "ID2"}}
  26. Defining an object type • Let's start with something really

    simple • Street has just one attribute - its name • It should be a string
  27. 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
  28. 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
  29. 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
  30. 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 ]
  31. Custom fields • Fields have their own resolver method •

    Allows you to share common custom resolving logic between multiple object types
  32. Defining an enum class Relationship(graphene.Enum): parent = 'parent' grandparent =

    'grandparent' child = 'child' grandchild = 'grandchild' spouse = 'spouse' sibling = 'sibling'
  33. 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
  34. Defining a field with arguments class Person(graphene.ObjectType): relationships = graphene.Field(

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

    lambda: Person, relationship=graphene.Argument( Relationship, required=True, ), )
  36. 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 ] …
  37. 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
  38. 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) ) )
  39. Using the schema directly >>> schema.execute(''' query { streets {

    name } } ''') {"data": {"streets": […]}}
  40. 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
  41. 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
  42. Mutations class MoveHouse(graphene.Mutation): class Input: person = graphene.Field(Person) house =

    graphene.Field(House) # output person = graphene.Field(Person) ok = graphene.Boolean()
  43. 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)
  44. 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
  45. 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()), ]
  46. What is React? • View layer of a traditional MVC

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

    an element • Class with a render() method
  48. 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); } });
  49. 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>; } });
  50. Virtual DOM • Renders in memory-light virtual DOM • Compares

    changes between trees • Flushes only changes to the document • Debounces rendering
  51. 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!
  52. 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
  53. 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
  54. 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
  55. Nodes query { streets { id name } } {"data":

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

    {"data": { "node": { "id": "XXXXXX", "name": "Afon Stryd", } }}
  57. 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
  58. Connections {"data": {"streets" { "name": "Afon Stryd", "houses": { "edges":

    [{ "cursor": "AAAAAA", "node": { "number": 1, } }, …
  59. Connections {"data": {"streets" { "name": "Afon Stryd", "houses": { "pageInfo":

    { "hasNextPage": true, "endCursor": "AAAAAA", } …
  60. Mutations • Mutations must both take and return a unique

    identifier • Allows multiple similar mutations to be checked individually by the framework
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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":[ ], …
  68. 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
  69. Webpack config var BundleTracker = require('webpack-bundle- tracker') plugins: [ new

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

    /node_modules/, loader: 'babel-loader', } ] }
  71. 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