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. An introduction to
    Graphene and Relay

    View Slide

  2. Part 0: Introduction

    View Slide

  3. Who am I?
    • Marc Tamlyn
    • Django Core Developer
    • Graphene contributor
    • Occasional Javascript developer
    • Work at Photocrowd

    View Slide

  4. Workshop structure
    • Split into four main parts
    • GraphQL
    • Graphene
    • React
    • Relay

    View Slide

  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

    View Slide

  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

    View Slide

  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!

    View Slide

  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

    View Slide

  9. Part 1: GraphQL

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  13. Scalars
    • Int
    • Float
    • String
    • Boolean
    • ID
    • Can define your own - needs to be JSON serialisable

    View Slide

  14. Lists and nulls
    • List of any other type
    • Fields are nullable unless specified as non-null

    View Slide

  15. Enumerations types
    • First class support for enums
    • Collection of unique string names
    • Allows query-time validation of arguments

    View Slide

  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

    View Slide

  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

    View Slide

  18. Schema
    • Schema is an ObjectType
    • Must have a query field
    • May have a mutation field

    View Slide

  19. Queries
    • Written in GraphQL query language
    • Returns JSON

    View Slide

  20. A simple query
    query {
    streets {
    name
    }
    }
    {"data": {
    "streets": [
    {"name": "Afon Stryd"},
    {"name": "Glyn Stryd"},
    {"name": "Ysgol Stryd"}
    ]
    }}

    View Slide

  21. Nesting
    query {
    streets {
    name
    houses {
    number
    }
    }
    }
    {"data": {
    "streets": [
    {"name": "Afon Stryd",
    "houses": [
    {"number": 1},
    {"number": 2},
    {"number": 3},
    ]},

    View Slide

  22. Multiple queries
    query {
    streets {
    name
    }
    houses {
    name
    }
    }
    {"data": {
    "streets": [
    {"name": "Afon Stryd"

    ]
    "houses": [
    {"name": "1 Afon
    Stryd"}

    View Slide

  23. Arguments and variables
    query {
    people {
    name
    family
    relationships(relationship: child) {
    name
    family
    }
    }
    }

    View Slide

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

    View Slide

  25. Arguments and variables
    {"data": {
    "people": [
    {"name": "Lowri",
    "family": "Jones",
    "relationships": [
    {"name": "Dafydd",
    "family": "Jones"},
    {"name": "Bethan",
    "family": "Jones"}]

    View Slide

  26. Fragments and aliases
    fragment F0 on Person {
    name
    residence {
    name
    }
    }

    View Slide

  27. Fragments and aliases
    query {
    people {
    name
    family
    children: relationships(relationship:
    child) {...F0}
    parents: relationships(relationship:
    parent) {...F0}
    }
    }

    View Slide

  28. Fragments and aliases
    "people": [
    {"name": "Lowri",
    "family": "Jones",
    "children": [
    {"name": "Dafydd",
    "residence": {
    "name": "1 Glyn Stryd"}
    },
    "parents": []},

    View Slide

  29. Inline fragments
    {
    search(q: "ysgol") {
    __typename
    ... on Street {
    name
    }
    ... on House {
    street {
    name
    }
    number
    }
    }
    }

    View Slide

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

    ]

    View Slide

  31. Directives
    query People($includeChildren: Boolean!) {
    people {
    name
    family
    relationships(relationship: child) @include (if:
    $includeChildren) {
    name
    }
    }
    }
    variables {"includeChildren": false}

    View Slide

  32. Directives
    {
    "data": {
    "people": [
    {
    "name": "Lowri",
    "family": "Jones"
    },

    }

    View Slide

  33. Have an explore!
    https://graphene-tutorial.herokuapp.com/plain/

    View Slide

  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

    View Slide

  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

    View Slide

  36. Making a mutation
    mutation Move($input: MoveInput!) {
    move(input: $input) {
    name
    residence {
    name
    }
    }
    }
    variables {"input": {"person": "ID1",
    "residence": "ID2"}}

    View Slide

  37. Part 2: Graphene

    View Slide

  38. Defining an object type
    • Let's start with something really simple
    • Street has just one attribute - its name
    • It should be a string

    View Slide

  39. Defining an object type
    class Street(graphene.ObjectType):
    name = graphene.Field(graphene.String)
    And you're done!

    View Slide

  40. Defining an object type - shorthand
    class Street(graphene.ObjectType):
    name = graphene.String()

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    ]

    View Slide

  46. Custom fields
    • Fields have their own resolver method
    • Allows you to share common custom resolving logic
    between multiple object types

    View Slide

  47. Defining an enum
    • Declarative
    • Uses python 3's enum.Enum (or a backport)

    View Slide

  48. Defining an enum
    class Relationship(graphene.Enum):
    parent = 'parent'
    grandparent = 'grandparent'
    child = 'child'
    grandchild = 'grandchild'
    spouse = 'spouse'
    sibling = 'sibling'

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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
    ]

    View Slide

  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

    View Slide

  54. Union Type
    class Anything(graphene.types.union.Union):
    class Meta:
    types = (Person, House, Street)

    View Slide

  55. Creating your schema
    • Create a Query ObjectType
    • Plumb that into the schema

    View Slide

  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)
    )
    )

    View Slide

  57. Creating your schema
    schema = graphene.Schema(query=Query)
    schema_with_mutations = graphene.Schema(
    query=Query,
    mutation=Mutation,
    )

    View Slide

  58. Using the schema directly
    >>> schema.execute('''
    query {
    streets {
    name
    }
    }
    ''')
    {"data": {"streets": […]}}

    View Slide

  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

    View Slide

  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

    View Slide

  61. Mutations
    class MoveHouse(graphene.Mutation):
    class Input:
    person = graphene.Field(Person)
    house = graphene.Field(House)
    # output
    person = graphene.Field(Person)
    ok = graphene.Boolean()

    View Slide

  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)

    View Slide

  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

    View Slide

  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()),
    ]

    View Slide

  65. Part 3: React

    View Slide

  66. What is React?
    • View layer of a traditional MVC
    • Virtual DOM
    • Single directional data flow

    View Slide

  67. What is a component?
    • Takes input (props)
    • Returns an element
    • Class with a render() method

    View Slide

  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);
    }
    });

    View Slide

  69. JSX
    • Preprocessor for React.createElement()
    • Compile using your JS preprocessor of choice

    View Slide

  70. JSX
    var houses = React.createClass({
    render: function() {
    var photos = this.props.houses.map((house) => {
    return number=house.number
    streetName=house.street.name
    >;
    });
    return { houses };
    }
    });

    View Slide

  71. Virtual DOM
    • Renders in memory-light virtual DOM
    • Compares changes between trees
    • Flushes only changes to the document
    • Debounces rendering

    View Slide

  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!

    View Slide

  73. Part 4: Relay

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  77. Nodes
    class StreetNode(DjangoObjectType):
    class Meta:
    model = Street
    only_fields = ['name']
    interfaces = (relay.Node,)

    View Slide

  78. Nodes
    query {
    streets {
    id
    name
    }
    }
    {"data": {
    "streets": [
    {"id": "XXXXXX",
    "name": "Afon Stryd"},
    {"id": "YYYYYY",

    "name": "Glyn Stryd"},
    {"id": "ZZZZZZ",

    "name": "Ysgol Stryd"}
    ]
    }}

    View Slide

  79. Nodes
    query {
    node(id: "XXXXXX")
    {
    id
    name
    }
    }
    {"data": {
    "node": {
    "id": "XXXXXX",
    "name": "Afon Stryd",
    }
    }}

    View Slide

  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

    View Slide

  81. Connections
    class StreetNode(DjangoObjectType):
    houses = relay.ConnectionField(
    HouseNode)
    def resolve_houses(self, args, ctx, info):
    return self.house_set.all()

    View Slide

  82. Connections
    query {
    streets {
    name
    houses(first: 1) {
    edges {
    cursor
    node {
    number
    }
    }
    }
    }
    }

    View Slide

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

    View Slide

  84. Connections
    query {
    streets {
    name
    houses(first: 1) {
    pageInfo {
    hasNextPage
    endCursor
    }
    }
    }
    }

    View Slide

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

    View Slide

  86. Connections
    query {
    streets {
    name
    houses(first: 1, after: "AAAAAA") {

    }
    }
    }

    View Slide

  87. Mutations
    • Mutations must both take and return a unique identifier
    • Allows multiple similar mutations to be checked
    individually by the framework

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  93. Mutations
    • Must receive and return the client identifier
    • Just use relay.ClientIDMutation

    View Slide

  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

    View Slide

  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":[
    ],

    View Slide

  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
    --out schema.json

    View Slide

  97. Webpack config
    var BundleTracker = require('webpack-bundle-
    tracker')
    plugins: [
    new BundleTracker({
    filename: './build/webpack-stats.json'
    }),
    ],

    View Slide

  98. Webpack config
    module: {
    loaders: [
    {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    }
    ]
    }

    View Slide

  99. package.json config
    "metadata": {
    "graphql": {
    "schema": "./schema.json"
    }
    }

    View Slide

  100. Conclusion

    View Slide

  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

    View Slide