# GraphQL 从入门到放弃 2018.5 @AlloVince --- ## 什么是GraphQL A query language for your API - language = DSL --- ## 举个栗子 schema: ```graphql { hello: String! } ``` ---- query document: ```graphql { hello } ``` response: ```json { "hello": "world" } ``` ---- ## 复杂一点的栗子 query document: ```graphql 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: ``` json { "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 ---- ![](https://s2.ax1x.com/2019/05/07/Erh3ss.jpg) ---- [See more](https://dev-blog.apollodata.com/the-anatomy-of-a-graphql-query-6dffa9e9e747) --- ## 数据类型 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 ``` ``` js 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 ![](https://ws4.sinaimg.cn/large/9150e4e5ly1fm76lfipijg20hs0hsdhl.gif) ---- Graph = 图 GraphQL = 遍历图的DSL ---- - 图的问题 - DSL的问题 ---- - DSL本身的问题 - 语言支持看脸 - IDE支持看脸 - 周边不完备 - 重构火葬场 - 花括号地狱 - 鸡肋的mutation - 不能操作,动态运算 - 容易出现性能问题 (白名单) - 需要配合Facebook其他设施才能发挥威力 ---- 结论? --- ## 进阶 - Introspection 内省 - Resolvers - DataLoader - Connection - Relay --- ### Introspection 内省 GraphiQL init request ``` graphql 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服务 ```js 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: ```js @GraphqlSchema(graphql` extend type Post { text: Text } `) text: post => entities.get('BlogTexts').findOne({ where: { postId: post.id }})) ``` ``` sql 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 ```js 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), ``` ``` sql 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 示例 ```graphql { user { id name friends(first: 10, after: "opaqueCursor") { edges { cursor node { id name } } pageInfo { hasNextPage } } } } ``` [Relay Cursor Connections Specification](https://facebook.github.io/relay/graphql/connections.htm) ---- #### Edge ![](https://cdn-images-1.medium.com/max/1600/1*z0-1RpRpVn_i-92AMR_2BA.png) ---- #### 分页比较 - users(page: 1, pageSize: 10) - users(offset: 0, limit 10) ---- ![](https://cdn-images-1.medium.com/max/1600/1*VGCj1SK3VCdWAleXK_rvkg.png) ---- ![](https://cdn-images-1.medium.com/max/1600/1*xfqxO28vw5G1vVIDD8HihQ.png) ---- #### 实现 users(first: 10) ```sql SELECT * FROM users ORDER BY id ASC LIMIT 10; ``` users(first: 10, after: "{ cursor: 100 }") ```sql SELECT * FROM users ORDER BY id ASC OFFSET 100 LIMIT 10; ``` users(last: 10) ```sql SELECT * FROM users ORDER BY id DESC LIMIT 10; ``` users(last: 10, before: "{ cursor: 100 }") ```sql SELECT * FROM users ORDER BY id DESC OFFSET 100 LIMIT 10; ``` ---- users(first: 10, last: 10) ```sql 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 }") ```sql 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 }") ```sql SELECT * FROM users WHERE id < 999 ORDER BY id DESC LIMIT 10; ``` ---- 当order不为主键 users(first: 10, after: "{ id: 999, cursor: 100 }", order: "-createdAt") ```sql SELECT * FROM users ORDER BY createdAt DESC, id ASC OFFSET 100 LIMIT 10; ``` ---- 问题 - 最好禁止first / last同时出现 - 有before, orderBy必须为ASC; 有after, orderBy必须为DESC; - 能部分解决翻页后数据不稳定的问题 - 情况: 按唯一索引排序,且唯一索引为数字 - 只适合有时序的信息流, 传统的分页跳转很麻烦 --- ## Relay - 对API有入侵 - 声明式获取数据 - 图查询缓存管理 ---- ```js import React from 'react' import { createFragmentContainer, graphql } from 'react-relay' const BlogPostPreview = props => { return (
{props.post.title}
) } 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 ```js 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 - https://philsturgeon.uk/api/2017/01/24/graphql-vs-rest-overview/ - https://dev-blog.apollodata.com/explaining-graphql-connections-c48b7c3d6976