Skip to content

Instantly share code, notes, and snippets.

@AlloVince AlloVince/graphql.md
Last active May 9, 2019

Embed
What would you like to do?
npm install -g reveal-md && wget -q -O graphql.md https://gist.githubusercontent.com/AlloVince/8ba1c92890c74cc7f4e68f09c79ec0d1/raw/graphql.md && reveal-md graphql.md

GraphQL

从入门到放弃

2018.5 @AlloVince


什么是GraphQL

A query language for your API

  • language = DSL

举个栗子

schema:

{
  hello: String!
}

query document:

{
  hello
}

response:

{
  "hello": "world"
}

复杂一点的栗子

query document:

query ($q: String = "magnet") {
  search(first: 10, query: $q, type: REPOSITORY) {
    pageInfo {
      startCursor
      endCursor
      hasNextPage
      hasPreviousPage
    }
    repositoryCount
    edges {
      cursor
      node {
        ... on Repository {
          id
          nameWithOwner
          object(expression: "master:README.md") {
            commitUrl
            ... on Blob {
              text
            }
          }
        }
      }
    }
  }
}

response:

{
  "data": {
    "search": {
      "pageInfo": {
        "startCursor": "Y3Vyc29yOjE=",
        "endCursor": "Y3Vyc29yOjE=",
        "hasNextPage": true,
        "hasPreviousPage": false
      },
      "repositoryCount": 3338,
      "edges": [
        {
          "cursor": "Y3Vyc29yOjE=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnkyMjA2MjU5Ng==",
            "nameWithOwner": "premnirmal/Magnet",
            "object": {
              "commitUrl": "https://github.com/premnirmal/Magnet/commit/4e50f1b02145d34b2051858a440bb1da30463843",
              "text": "..."
            }
          }
        }
      ]
    }
  }
}

十分复杂的栗子


基本概念

  • Schema
    • 定义了服务所支持的类型(Types)和指令(Directives)

  • Query Document
    • 定义了一次数据查询请求, 由多个操作(Operations)和片段(Fragments)构成
    • Operations
      • query
      • mutation
      • *subscription

语法组成

  • Fields
  • Arguments
  • Aliases
  • Fragments
  • Operation Name
  • Variables
  • Directives
  • Inline Fragments


See more


数据类型

  1. Scalars
  2. Objects
  3. Interfaces
  4. Unions
  5. Enums
  6. Input Objects
  7. Lists
  8. Non-Null

Scalars

  • Int
  • Float
  • String
  • Boolean
  • ID

自定义类型

scalar PhoneNumber
const PHONE_NUMBER_REGEX = new RegExp(
  /^\+\d{11,15}$/,
);

export default new GraphQLScalarType({
  name: 'PhoneNumber',

  description:
    'A field whose value conforms to the standard E.164 format as specified in: https://en.wikipedia.org/wiki/E.164. Basically this is +17895551234.',

  serialize(value) {
    if (typeof value !== 'string') {
      throw new TypeError(`Value is not string: ${value}`);
    }

    if (!PHONE_NUMBER_REGEX.test(value)) {
      throw new TypeError(`Value is not a valid phone number of the form +17895551234 (10-15 digits): ${value}`);
    }

    return value;
  },

  parseValue(value) {
    if (typeof value !== 'string') {
      throw new TypeError(`Value is not string: ${value}`);
    }

    if (!PHONE_NUMBER_REGEX.test(value)) {
      throw new TypeError(`Value is not a valid phone number of the form +17895551234 (10-15 digits): ${value}`);
    }

    return value;
  },

  parseLiteral(ast) {
    if (ast.kind !== Kind.STRING) {
      throw new GraphQLError(
        `Can only validate strings as phone numbers but got a: ${ast.kind}`,
      );
    }

    if (!PHONE_NUMBER_REGEX.test(ast.value)) {
      throw new TypeError(`Value is not a valid phone number of the form +17895551234 (10-15 digits): ${ast.value}`);
    }

    return ast.value;
  },
});

GraphQL VS RESTFul

RESTFul GraphQL
定义 an architectural concept a query language
理念 服务端主导 客户端主导

RESTFul GraphQL
类型 一般是JSON 😐 可扩展的类型系统 😄
迭代 一般基于URI 😐 可标记过期 😄
字段 难以精确控制 😭 可以精确控制 😄
关联 需要多次请求 😭 一次请求 😄
文档 需要单独维护 😭 强一致 😄

RESTFul GraphQL
了解API参数 只能通过文档 😐 内省 😄
调试工具 Swagger 😐 GraphiQL 😄
实现 任何语言 😄 异步语言友好 😐

RESTFul GraphQL
缓存控制 服务端为主 😄 客户端必须小心 😭
缓存粒度 Endpoint级别 😄 图缓存 😭
问题追踪 基于简单Log 😄 需要辅助系统 😭
权限管理 API级别 😄 代码级别 😭
错误处理 简单 😄 复杂 😭
限流/降级 容易 😄 复杂 😭

GraphQL: The Evolution of the API


Graph = 图

GraphQL = 遍历图的DSL


  • 图的问题
  • DSL的问题

  • DSL本身的问题
  • 语言支持看脸
  • IDE支持看脸
  • 周边不完备
  • 重构火葬场
  • 花括号地狱
  • 鸡肋的mutation
  • 不能操作,动态运算
  • 容易出现性能问题 (白名单)
  • 需要配合Facebook其他设施才能发挥威力

结论?


进阶

  • Introspection 内省
  • Resolvers
  • DataLoader
  • Connection
  • Relay

Introspection 内省

GraphiQL init request

query IntrospectionQuery {
  __schema {
    queryType {
      name
    }
    mutationType {
      name
    }
    subscriptionType {
      name
    }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args {
        ...InputValue
      }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description
  fields(includeDeprecated: true) {
    name
    description
    args {
      ...InputValue
    }
    type {
      ...TypeRef
    }
    isDeprecated
    deprecationReason
  }
  inputFields {
    ...InputValue
  }
  interfaces {
    ...TypeRef
  }
  enumValues(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
  }
  possibleTypes {
    ...TypeRef
  }
}

fragment InputValue on __InputValue {
  name
  description
  type {
    ...TypeRef
  }
  defaultValue
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}

npm install graphql-cli
graphql get-schema

Resolvers

实现一个GraphQL服务

const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');
const { makeExecutableSchema } = require('graphql-tools');

const typeDefs = `
  type Query { books: [Book] }
  type Book { title: String, author: String }
`;

const resolvers = {
  Query: { books: () => [{ title: "foo", author: "bar"}] },
};

const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
});

const app = require('express')();
app.use('/graphql', require('body-parser').json(), graphqlExpress({ schema }));
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));
app.listen(3000, () => {});

DataLoader

Before:

@GraphqlSchema(graphql`
    extend type Post {
        text: Text
    }
`)
text: post => entities.get('BlogTexts').findOne({ where: { postId: post.id }}))
SELECT * FROM `eva_blog_texts` AS `BlogTexts` WHERE `BlogTexts`.`postId` =  1;
SELECT * FROM `eva_blog_texts` AS `BlogTexts` WHERE `BlogTexts`.`postId` =  2;
SELECT * FROM `eva_blog_texts` AS `BlogTexts` WHERE `BlogTexts`.`postId` =  3;
....

After

const textDataLoader = new DataLoader(async ids =>
  entities.get('BlogTexts').findAll({
    where: {
      postId: ids
    },
    order: [[sequelize.fn('FIELD', sequelize.col('postId'), ...ids)]]
  }));
@GraphqlSchema(graphql`
    extend type Post {
        text: Text
    }
`)
text: post => textDataLoader.load(post.id),
SELECT * FROM `eva_blog_texts` AS `BlogTexts` WHERE `BlogTexts`.`postId` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) ORDER BY FIELD(`postId`, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Connection

示例

{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor") {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

Relay Cursor Connections Specification


Edge


分页比较

  • users(page: 1, pageSize: 10)
  • users(offset: 0, limit 10)



实现

users(first: 10)

SELECT * FROM users ORDER BY id ASC LIMIT 10;

users(first: 10, after: "{ cursor: 100 }")

SELECT * FROM users ORDER BY id ASC OFFSET 100 LIMIT 10;

users(last: 10)

SELECT * FROM users ORDER BY id DESC LIMIT 10;

users(last: 10, before: "{ cursor: 100 }")

SELECT * FROM users ORDER BY id DESC OFFSET 100 LIMIT 10;

users(first: 10, last: 10)

SELECT * FROM (SELECT * FROM users ORDER BY id ASC LIMIT 10)
UNION
SELECT * FROM (SELECT * FROM users ORDER BY id DESC LIMIT 10);

当cursor中包含主键信息

users(first: 10, after: "{ id: 999 }") users(first: 10, after: "{ id: 999, cursor: 100 }")

SELECT * FROM users WHERE id > 999 ORDER BY id ASC LIMIT 10;

users(last: 10, before: "{ id: 999 }") users(last: 10, before: "{ id: 999, cursor: 100 }")

SELECT * FROM users WHERE id < 999 ORDER BY id DESC LIMIT 10;

当order不为主键

users(first: 10, after: "{ id: 999, cursor: 100 }", order: "-createdAt")

SELECT * FROM users ORDER BY createdAt DESC, id ASC OFFSET 100 LIMIT 10;

问题

  • 最好禁止first / last同时出现
  • 有before, orderBy必须为ASC; 有after, orderBy必须为DESC;
  • 能部分解决翻页后数据不稳定的问题
    • 情况: 按唯一索引排序,且唯一索引为数字
  • 只适合有时序的信息流, 传统的分页跳转很麻烦

Relay

  • 对API有入侵
  • 声明式获取数据
  • 图查询缓存管理

import React from 'react'
import { createFragmentContainer, graphql } from 'react-relay'

const BlogPostPreview = props => {
  return (
    <div key={props.post.id}>{props.post.title}</div>
  )
}

export default createFragmentContainer(BlogPostPreview, {
  post: graphql`
        fragment BlogPostPreview_post on BlogPost {
            id
            title
        }
    `
})

Server Side 最佳实践

  • 官方实现
  • Apollo方案 by Meteor 团队

问题点:

  • DB/远程服务都是有Entity的,Entity永远存在且属于Schema的子集
  • Schema需要扩展,扩展的代码不适合放在一起
  • Schemas/Resolvers 分开写的痛苦
  • Relay与数据库映射繁琐

GraphQL Boot


创建一个Connection

import { graphql, GraphqlSchema, Types, Connection } from 'graphql-boot';
export const resolver = {
  Query: {
    @GraphqlSchema(graphql`
        type PostListingEdge {
            cursor: String!
            node: Post
        }
        type PostListingConnection {
            totalCount: Int!
            pageInfo: PageInfo!
            edges: [PostListingEdge]
            nodes: [Post]
        }
        extend type Query {
            postListings(first: Int, after:String, last:Int, before: String, order:SortOrder): PostListingConnection
        }
    `)
    postListings: async (source, args) => {
      const {
        first, after, last, before, order = {
          field: 'id',
          direction: 'ASC'
        }
      } = args;
    
      const connection = new Connection({
        first,
        after,
        last,
        before,
        primaryKey: 'id',
        order: (new Types.SortOrder(order)).toString()
      });
    
      const query = connection.getSqlQuery();
      const { count, rows } = await entities.get('BlogPosts').findAndCountAll(query);
      connection.setTotalCount(count);
      connection.setNodes(rows);
      return connection.toJSON();
    }
  }
}

Refers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.