GraphQL in Django

力扣(LeetCode)李齐雨

http GET /user/1

{
  "name": "齐雨"
}

http GET /user_with_friends/1

{
  "name": "齐雨",
  "friends": {
    { "name": "太狼" },
    { "name": "蛋蛋" },
    { "name": "炸蛋" }
  }
}

http GET /user_and_avatar/1

{
  "name": "齐雨",
  "avatar": "https://cdn//qy.png"
}

http GET /user_and_small_avatar/1

{
  "name": "齐雨",
  "avatar": "https://cdn//qy_small.png"
}

http GET /user_with_friends_and_avatar/1

http GET /user_with_friends_and_small_avatar/1

http GET /v2/user/1

http GET /v3/user/1

http GET /v3/user_with_friends/1


query { 
  user(id: 1) {
    name
  }
}

 
{
  "data": {
    "user": {
      "name": "齐雨"
    }
  }
}

query { 
  user(id: 1) {
    name
    avatar
  }
}

 
{
  "data": {
    "user": {
      "name": "齐雨",
      "avatar": "https://cdn/qy.png"
    }
  }
}

query { 
  user(id: 1) {
    name
    avatar
    friends {
      name
      avatar
    }
  }
}

 
{
  "data": {
    "user": {
      "name": "齐雨",
      "avatar": "https://cdn/qy.png",
      "friends": [
        { "name": "太狼", "avatar": "a.png" },
        { "name": "蛋蛋", "avatar": "b.png" },
        { "name": "炸蛋", "avatar": "c.png" }
      ]
    }
  }
}

query { 
  user(id: 1) {
    name
    avatar
    friends(first: 2) {
      name
      avatar
    }
  }
}

 
{
  "data": {
    "user": {
      "name": "齐雨",
      "avatar": "https://cdn/qy.png",
      "friends": [
        { "name": "太狼", "avatar": "a.png" },
        { "name": "蛋蛋", "avatar": "b.png" }
      ]
    }
  }
}

query { 
  user(id: 1) {
    name
    profile {
      age
      avatar {
        url
        width
        height
      }
    }
  }
}

 
{
  "data": {
    "user": {
      "name": "齐雨",
      "profile": {
        "age": 18,
        "avatar": {
          "url": "https://cdn/qy.png",
          "width": 50,
          "height": 50
        }
      }
    }
  }
}

Type System

type User {
  name: String!
  avatar: String
  friends: [User]
}

Document

import graphene

class Query(graphene.ObjectType):
    hello = graphene.String()
    
    def resolve_hello(self, info):
        return 'World'


schema = graphene.Schema(query=Query)

schema.execute('''
  query {
    hello
  }
''')

Every field on every type is backed by a function called a resolver.

 

# settings.py
INSTALLED_APPS = (
    'django.contrib.staticfiles', # Required for GraphiQL
    'graphene_django',
)

GRAPHENE = {
    'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives
}
pip install "graphene-django>=2.0"
from django.urls import path
from graphene_django.views import GraphQLView

urlpatterns = [
    # ...
    path('graphql', GraphQLView.as_view(graphiql=True)),
]
from django.db import models

class UserModel(models.Model):
    name = models.CharField(max_length=100)
    avatar = models.CharField(max_length=100)

Django Model

from graphene_django import DjangoObjectType
import graphene

class User(DjangoObjectType):
    class Meta:
        model = UserModel

class Query(graphene.ObjectType):
    user = graphene.Field(User, id=graphene.Int())

    def resolve_user(self, info, id):
        return UserMode.objects.filter(id=id).first()

schema = graphene.Schema(query=Query)

GraphQL Schema

query = '''
    query {
      user(id: 1) {
        name
        avatar
      }
    }
'''

result = schema.execute(query)

Execute Query

class User(DjangoObjectType):
    class Meta:
        model = UserModel

GraphQL Schema

class User(DjangoObjectType):
    age = graphene.Int()
    
    def resolve_age(self, info):
        # get age data from user related table
      
    class Meta:
        model = UserModel

Error Handling

# views.py
from graphene_django.views import GraphQLView

class SentryGraphQLView(GraphQLView):
    def execute_graphql_request(self, *args, **kwargs):
        """Extract any exceptions and send them to Sentry"""
        result = super().execute_graphql_request(*args, **kwargs)
        if result.errors:
            for error in result.errors:
                try:
                    raise error.original_error
                except Exception:
      				# Track error with sentry...
        return result


# urls.py
from .views import SentryGraphQLView


urlpatterns = [
    url(r'^graphql', SentryGraphQLView.as_view(schema=schema)),
]
class SentryMiddleware(object):
    def on_error(self, error):
      # Track error with sentry...
      
    def resolve(self, next, root, args, context, info):
       return next(root, args, context, info).catch(self.on_error)
      
# models.py
result = schema.execute('THE QUERY', middleware=[SentryMiddleware()])

Performance

N+1 Problem

图片来自 paypal 工程博客

class Query(graphene.ObjectType):
    authors = graphene.List(Author)

    def resolve_authors(self, info):
        return AuthorModel.objects.all()
      
class Author(graphene.ObjectType):
  address = graphene.Field(Address)
  
  def resolve_address(self, info):
    return Address.objects.filter(id=self.address)
  
  class Meta:
    model = AuthorModel

schema = graphene.Schema(query=Query)

#query {                      
#  authors {      # fetches authors (1 query)
#    name       
#    address {    # fetches address for each author (N queries for N authors)
#      country
#    }
#  }
#}                # Therefore = N+1 round trips
class Query(graphene.ObjectType):
    authors = graphene.List(Author)

    def resolve_authors(self, info):
        # return AuthorModel.objects.all()
        return AuthorModel.objects.select_related('address')
      
class Author(graphene.ObjectType):
  address = graphene.Field(Address)
  
  def resolve_address(self, info):
    # return Address.objects.filter(id=self.address)
    return self.address
  
  class Meta:
    model = AuthorModel

schema = graphene.Schema(query=Query)

#query {                      
#  authors {      # fetches authors (1 query)
#    name       
#    address {    # get address from parent
#      country
#    }
#  }
#}               

Debug Middleware

GRAPHENE = {
    ...
    'MIDDLEWARE': [
        'graphene_django.debug.DjangoDebugMiddleware',
    ]
}

from graphene_django.debug import DjangoDebug

class Query(graphene.ObjectType):
    # ...
    debug = graphene.Field(DjangoDebug, name='_debug')

schema = graphene.Schema(query=Query)
{
  user(id: 1) {
    name
  }
  # Here is the debug field that will output the SQL queries
  _debug {
    sql {
      rawSql
    }
  }
}

Dataloader

class AddressLoader(DataLoader):
    def batch_load_fn(self, keys):
        addresses = {address.id: address for address in Address.objects.filter(id__in=keys)}
        return Promise.resolve([addresses.get(address_id) for address_id in keys])
class Query(graphene.ObjectType):
    authors = graphene.List(Author)

    def resolve_authors(self, info):
        return AuthorModel.objects.all()
      
class Author(graphene.ObjectType):
    address = graphene.Field(Address)
  
    def resolve_address(self, info):
      # return Address.objects.filter(author_id=self.id)
      return AddressLoader.load(self.address_id)
  
    class Meta:
      model = AuthorModel

schema = graphene.Schema(query=Query)

#query {                      
#  authors {      # fetches authors (1 query)
#    name       
#    address {    # AddressLoader batch query address (1 query)
#      country
#    }
#  }
#}               

Huge List

import graphene


class UserNode(graphene.ObjectType):
    id = graphene.Int()


class Query(graphene.ObjectType):
    users = graphene.List(UserNode)

    def resolve_users(self, info):
        return users


users = [UserNode(id=index) for index in range(0, 100_000)]

schema = graphene.Schema(query=Query)

schema.execute('{ users { id } }').data
import graphene
import graphql.execution.executor

# skip complete_value


class UserNode(graphene.ObjectType):
    id = graphene.ID()


class Query(graphene.ObjectType):
    users = graphene.List(UserNode)

    def resolve_users(self, info):
        return RawGraphQLResult([{ 'users': { 'id': user.id }} for user in users])


users = [UserNode(id=index) for index in range(0, 100_000)]

schema = graphene.Schema(query=Query)

schema.execute('{ users { id } }').data

“JIT”

1. Parse: Query => AST

2. Validate: The AST validated against the schema. Checks for correct query syntax and if the fields exist.

3. Execute: The runtime walks through the AST, starting from the root of the tree, invokes resolvers, collects up results, and emits JSON.

query HelloWorld {
  hello
}


def execute(root, variables=None, operation_name=None):
    if operation_name is None or operation_name == 'HelloWorld':
        selection_type = schema.get_type('Query')
        hello_resolver = selection_type.get_field('hello').resolver
        info = ResolveInfo(...)
        return {
            'hello': hello_resolver(root, info)
        }

Thanks!

Made with Slides.com