从入门到放弃
2018.5 @AlloVince
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
- Scalars
- Objects
- Interfaces
- Unions
- Enums
- Input Objects
- Lists
- Non-Null
- 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;
},
});
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
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
实现一个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, () => {});
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);
示例
{
user {
id
name
friends(first: 10, after: "opaqueCursor") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
Relay Cursor Connections Specification
- 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;
- 能部分解决翻页后数据不稳定的问题
- 情况: 按唯一索引排序,且唯一索引为数字
- 只适合有时序的信息流, 传统的分页跳转很麻烦
- 对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
}
`
})
- 官方实现
- 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();
}
}
}