Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL技术分享(上)概念篇 #1

Open
JeniTurtle opened this issue Jul 11, 2019 · 0 comments
Open

GraphQL技术分享(上)概念篇 #1

JeniTurtle opened this issue Jul 11, 2019 · 0 comments

Comments

@JeniTurtle
Copy link
Owner

JeniTurtle commented Jul 11, 2019

GraphQL技术分享(上)概念篇


目录大纲

1、什么是GraphQL

  • GraphQL介绍
  • GraphQL生态

2、GraphQL核心概念

  • Type System
  • Query & Mutation
  • Resolver
  • Introspection
  • Validation

3、能为团队带来什么

  • GraphQL能提供哪些帮助
  • 有哪些问题需要解决
  • RPC vs REST vs GraphQL

一、GraphQL介绍

1、什么是GraphQL?

简单的说,GraphQL是由Facebook开源的一套**“用于API的查询语言”,全称是“Graph Query Language”**。
image-1

不同于数据库的SQL,GraphQL是一种前后端数据交互的规范,只是通过类似SQL查询的方式,对后端提供的接口数据,进行自定义查询,但是它的数据源需要开发者自己定义,而不是任由前端人员传入GraphQL语句,直接读取数据库。

它不局限于编程语言,任何语言都可以实现这套规范。
https://graphql.cn/code/

2、GraphQL生态

awesome-graphql上列举了Github上面开源的并且十分有用的graphql相关的服务端、客户端以及生态链相关的其他工具。
https://github.com/chentsulin/awesome-graphql

  • GraphiQL:
    image-1

  • Graphql-Network:
    image-1

  • GraphQL-Voyager:
    image-1


二、GraphQL核心概念

1、Type System

GraphQL 的强大表达能力主要还是来自于它完备的类型系统,与 REST 不同,它将整个 Web 服务中的全部资源看成一个有连接的图,而不是一个个资源孤岛,在访问任何资源时都可以通过资源之间的连接访问其它的资源。

image-1

总而言之,GraphQL中的Type,就是描述和表达这些视图模型最基本的要素。

type Issue {
    title: String,
    content: String
}
type User {
    id: String,
    name: String,
    age: Integer,
    issue: [Issue]
}

GraphQL 不单单支持简单类型,还支持一些其他类型,如 Object, Enum, List, NotNull 这些常见的类型,还有 Interface, Union, InputObject 这几个特殊类型。

2、Query & Mutation

GraphQL的查询语法同我们现在所使用的有一大不同是,传输的数据结构并不是 JSON 对象,而是一个字符串,这个字符串描述了客户端希望服务端返回数据的具体结构。

Query,查询操作。

query fetchArticle {
    article {
        id,
        createDate,
        title,
        subtitle,
        content,
        tags {
            name,
            label
        }
    }
}
{
    "data": {
        "article": {
            "id": "3",
            "createDate": "2016-08-01",
            "title": "GraphQL基础概念",
            "subtitle": "A query language created by Facebook for decribing data requirements on complex application data models",
            "content": "省略...",
            "tags": [{
                "name": "graphql",
                "label": "GraphQL"
            }]
        }
    }
}

Motation,执行修改操作。

mutation addArticle($input: Article!) {
    addArticle(input: $input) {
      id
      title
    }
}
{
    "data": {
        "addArticle": {
            "id": "3",
            "title": "GraphQL基础概念"
        }
    }
}

在查询字段时,是并行执行,而变更字段时,是线性执行,一个接着一个

3、Resolver

GraphQL的执行过程:

1、Validating Requests:主要是检查传入的schema、document、variables是否符合规范格式。

2、Coercing Variable Values:检查客户端请求变量的合法性,需要和schema进行对比。

3、Executing Operations:执行客户端请求的方法与之对应的resolver函数

4、Executing Selection Sets:搜罗客户端请求需要返回的字段集合

5、Executing Fields:执行每个字段,需要进行递归

参考链接:https://juejin.im/post/5ceb1e28f265da1bb80c0b70

现在让我们用一个例子来描述当一个查询请求被执行的全过程。

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}
{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

你可以将 GraphQL 查询中的每个字段视为返回子类型的父类型函数或方法。事实上,这正是 GraphQL 的工作原理。每个类型的每个字段都由一个 resolver 函数支持,该函数由 GraphQL 服务器开发人员提供。当一个字段被执行时,相应的 resolver 被调用以产生下一个值。

如果字段产生标量值,例如字符串或数字,则执行完成。如果一个字段产生一个对象,则该查询将继续执行该对象对应字段的解析器,直到生成标量值。GraphQL 查询始终以标量值结束。

4、Introspection

我们有时候会需要去问 GraphQL Schema 它支持哪些查询。GraphQL 通过内省系统让我们可以做到这点!

{
  __schema {
    types {
      name
    }
  }
}
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "Episode"
        },
        {
          "name": "Character"
        },
        {
          "name": "ID"
        },
        {
          "name": "String"
        },
        {
          "name": "Int"
        },
        {
          "name": "FriendsConnection"
        },
        {
          "name": "FriendsEdge"
        },
        {
          "name": "PageInfo"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "Review"
        },
        {
          "name": "SearchResult"
        },
        {
          "name": "Human"
        },
        {
          "name": "LengthUnit"
        },
        {
          "name": "Float"
        },
        {
          "name": "Starship"
        },
        {
          "name": "Droid"
        },
        {
          "name": "Mutation"
        },
        {
          "name": "ReviewInput"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        }
      ]
    }
  }
}
  • Query, Character, Human, Episode, Droid - 这些是我们在类型系统中定义的类型。
  • String, Boolean - 这些是内建的标量,由类型系统提供。
  • __Schema, __Type, __TypeKind, __Field, __InputValue, __EnumValue, __Directive - 这些有着两个下划线的类型是内省系统的一部分。

5、Validation

通过使用类型系统,你可以预判一个查询是否有效。这让服务器和客户端可以在无效查询创建时就有效地通知开发者,而不用依赖运行时检查。

{
  hero {
    ...NameAndAppearancesAndFriends
  }
}

fragment NameAndAppearancesAndFriends on Character {
  name
  appearsIn
  friends {
    ...NameAndAppearancesAndFriends
  }
}

片段不能引用其自身或者创造回环,因为这会导致结果无边界。

{
  "errors": [
    {
      "message": "Cannot spread fragment \"NameAndAppearancesAndFriends\" within itself.",
      "locations": [
        {
          "line": 11,
          "column": 5
        }
      ]
    }
  ]
}

三、能为团队带来什么

1、GraphQL能提供哪些便捷?

  • 单一入口

    简化了**“版本管理”“路径管理”**。

    image-1


  • 数据聚合

    image-1

    这里的数据聚合,分前端和后端两种意义:

    从前端的意义上来讲,可以做到多个查询接口合并成一个请求,后端解析后并行执行。

    从后端的意义上讲,是封装了多个服务提供的数据。

    image-1

    image-1

    image-1


  • 避免数据冗余

    image-1

    我们在传统的 RESTful 处理冗余的数据字段大约有这么三种处理方式:

    1. 前端不去展示这些字段;
    2. 加一个中间层服务去筛选这些字段,然后再返回终端来展示出来;
    3. 前端和后端去做约定,如果说这一个接口这一个字段已经不要,可以和后端商量一下把这个删掉,但是有一种情况可能造成冗余字段删不掉的,那就是后端的同学做这个接口可能是“万能接口”,也就是说这个接口在这个页面会用,在另外一个页面也能用,在这个应用会用,在另外一个应用也可能会用,多端之间存在部分数据共享,后端同学为了方便可能会写这么一个“万能”的接口来应付这种情况,久而久之,发现字段冗余到很多了,但是随便删除又可能会影响到很多地方,导致这个接口大而不能动,所以前后端都不得不忍受它。

    在GraphQL中如何去做:

    image-1


  • 接口文档和模型关系

    GraphQL 类型定义的时候我们可以对类型以及类型的属性增加描述 (description) , 这相当于是对类型做注释,当类型被编译以后就可以在相应的工具上面看到我们编辑的类型详情。

    image-1

    模型关系演示:https://apis.guru/graphql-voyager/


* 约束规范
GraphQL自带的参数类型效验,以及必填、选填等基础效验。

### 2、我们需要解决哪些问题
  • 权限认证

    基本上,有三种情况会发生:

    1、已经登录的用户发出GraphQL查询,未登录的用户不可以。认证在非GraphQL节点完成。
    2、所有用户都可以发出GraphQL查询,未登录用户可以使用其中的一个子集,认证在非GraphQL节点完成。
    3、所有用户都可以发出GraphQL查询,认证就由GraphQL节点完成。

非GraphQL节点的处理认证:
是指在resolver执行之前,也就是请求在一个比GraphQL路由更早的中间件处理,之后,请求才会到达GraphQL代码。我们知道请求是从哪里来的。我们甚至都可以在请求到达GraphQL代码以前,把请求重定向到登录页面。


  • N+1 问题

    这里的 N + 1 就是db operation的次数

    # schemas/article.py
    class ArticleSchema(SQLAlchemyObjectType):
        author = graphene.Field("schemas.AuthorSchema", description="文章作者信息")
    
        def resolve_author(self, info):
            return AuthorManager.get_one(id=self.author_id)
    
        class Meta:
            model = ArticleModel
            description = "文章Schema"
    SELECT * FROM `articles` LIMIT 0, 20;
    SELECT * FROM `authors` WHERE `id` = 1;
    SELECT * FROM `authors` WHERE `id` = 2;
    SELECT * FROM `authors` WHERE `id` = 3;
    ...
    SELECT * FROM `authors` WHERE `id` = N;

    为了防止N+1问题,社区为GraphQL提供了一个解决方案: DataLoader。其原理就是,在需要查询数据库的时候将查询进行延迟,等到拿到所有的查询需求之后再一次性查询出来。在graphene里面,批量查询可以这样写:

    # managers/author.py
    class AuthorsDataLoader(DataLoader):
        def batch_load_fn(self, ids):
            query = DBSession().query(AuthorModel).filter(AuthorModel.id.in_(ids))
            articles = dict([(article.id, article) for article in query.all()])
            return Promise.resolve([articles.get(id, None) for id in ids])

    最终,仅需要两次数据库查询就完成了两个批量查询,即:

    SELECT * FROM `articles` LIMIT 0, 20;
    SELECT * FROM `authors` WHERE `id` IN (1, 2, 3, ..., N);
  • 缓存

可以提供对象的标识符以便客户端构建丰富的缓存

在基于入口端点的 API 中,客户端可以使用 HTTP 缓存来确定两个资源是否相同,从而轻松避免重新获取资源。这些 API 中的 URL 是全局唯一标识符,客户端可以利用它来构建缓存。然而,在 GraphQL 中,没有类似 URL 的基元能够为给定对象提供全局唯一标识符。

方案:使用全局唯一ID

一个可行的模式是将一个字段(如 id)保留为全局唯一标识符。这些文档中使用的示例模式使用此方法

{
  starship(id:"3003") {
    id
    name
  }
  droid(id:"2001") {
    id
    name
    friends {
      id
      name
    }
  }
}
{
  "data": {
    "starship": {
      "id": "3003",
      "name": "Imperial shuttle"
    },
    "droid": {
      "id": "2001",
      "name": "R2-D2",
      "friends": [
        {
          "id": "1000",
          "name": "Luke Skywalker"
        },
        {
          "id": "1002",
          "name": "Han Solo"
        },
        {
          "id": "1003",
          "name": "Leia Organa"
        }
      ]
    }
  }
}

这是向客户端开发人员提供的强大工具。与基于资源的 API 使用 URL 作为全局唯一主键的方式相同,该系统中提供 id 字段作为全局唯一主键。

如果后端使用类似 UUID 的标识符,那么暴露这个全局唯一 ID 可能非常简单!如果后端对于每个对象并未分配全局唯一 ID,则 GraphQL 层可能需要构造此 ID。通常来说,将类型的名称附加到 ID 并将其用作标识符都很简单;服务器可能会通过 base64 编码使该 ID 不透明。

3、RPC vs REST vs GraphQL

  • RPC

    优点:
    轻量级的数据载体
    高性能
    开发人员实现简单

     

    缺点:
    对外应用不够灵活
    对于系统本身耦合性高
    因为RPC容易实现、轻量,因此很容易造成function explosion,或者违背开闭原则。


  • REST

    优点:
    对于系统本身耦合性低,调用者不再需要了解接口内部处理和实现细节。
    重复使用了一些 http 协议中的已定义好的部分状态动词,增强语义表现力
    API可以随着时间而不断演进

     

    缺点:
    缺少约束,缺少简单、统一的规范
    随着功能的迭代,会造成部分接口的数据冗余
    有时候调用api会比较繁琐,需要发送多条请求去获取数据


  • GraphQL

    优点:
    单一入口
    数据聚合
    避免数据冗余
    接口文档和模型关系
    约束规范

     

    缺点:
    本身的语法相比较REST和RPC均复杂一些
    实现方面需要配套 Batchcing 和 Caching 以解决性能瓶颈
    仍然是新鲜事物,很多技术细节仍然处于待验证状态

 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant