Skip to content

了解 graphql: schema 与 operation

Published: at 16:00

GraphQL 指南 · 第 3 篇,共 13 篇

从上文 hello, world 中可以看出 graphql 很关键的两个要素:schemaoperation

而当我们开发 web 应用时,schema 将会是服务端的主体,而 operation 存在于前端中,类似 REST 中的 API。

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

我们先抽出 schema 的代码部分,这里有 GraphQLSchemaGraphQLObjectTypeGraphQLString 等诸多 API。

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: "RootQueryType",
    fields: {
      hello: {
        type: GraphQLString,
        resolve() {
          return "hello, world";
        },
      },
    },
  }),
});

正如在 React 中使用jsx 简化了 React.createElement 的写法。graphql 对于 schema 也有一套它自己的 DSL (Domain Specified Language),也更为简单易懂。在代码中以 graphqlgql 作为文件名后缀,用法如下

# schama,在后端进行维护
type Query {
  hello: String
}

# query,在前端进行管理
{
  hello
}

以及查询结果

{
  hello: "hello, world";
}

PS: 在前端中所有查询的 gql 往往会通过 graphql/gql 后缀的文件来统一维护,这里有一份代码示例: shfshanyue/shici:query.gql

你看到这里,想必有两个疑问:

  1. 以上的 graphql 代表什么,以及我们如何书写 graphql
  2. 相比 js 代码,DSL 少了 resolve 函数,而它又是什么

Object Type 与 Field

这里引入 graphql 中的两个基本术语,Object TypeField。它们是组成 graphql 最基本的组件,如同细胞是生物体的基本单位。

这里来一个更复杂的 schema,如下所示

# schema
schema {
  query: Query
}

type Query {
  hello: String
  todos: [Todo!]!
}

type Todo {
  id: ID!
  title: String!
}

# query
{
  # 只能查询 Query 下的字段
  todos {
    id
    title
  }
  hello
}

如果说 graphql 是数据库的进一步抽象,则 object type 类似于 sql 中的 tablefield 类似于 sql 中的 column

那我们仔细审视以上示例,能从其中得到一些信息:

到了这里,你会发现,对于 graphql schema 的认识还有一些信息尚未涉及:IDString,它们被称作 scalar type,你可以理解为数据类型。 正是因为 scalar,graphql 才成为强类型查询语言。

Operation: query everyting

在 GraphQL 中,包括三种最基本的操作: QueryMutationSubscription

Querygraphql 的入口查询处,我们可以并且只可以查询 Query 下的任意字段 (field)。

因此,他组成了 graphql 最核心的功能: 查找你所需要的任何数据

# schema
type Query {
  hello: String
  todos: [Todo!]!
}

// 以下三个 query 一般会在前端进行统一管理
# query 1
{
  hello
}

# query 2
{
  todos {
    id
  }
}

# query 3
{
  hello
  todos {
    id
    title
  }
}

查询结果如下

{ hello: 'hello, world' }
{ todos: [{ id: 1 }] }
{ hello: 'hello, world', todos: [{ id: 1, title: 'learn react' }] }

在前端我们根据 Query 组合成各种查询,而我们为了在查询过程中方便辨认,可以为查询添加 operationName

query HELLO {
  hello
}

query TODOS {
  todos {
    id
  }
}

query HELLO_AND_TODOS {
  hello
  todos {
    id
    title
  }
}

Scalar Type

graphql 中有一些内置的 scalar 类型,用以表示 graphql 中 field 的数据类型,这也是 graphql 为强类型语言的基础。内置类型如下所示

正因为 scalar!,来保证了 graphql 的 query 是强类型的。所以当我们看到如下的 query 时,可以在前端大胆放心的使用: data.todos.map(todo => todo.title.replace(' ', ''))。既不用担心 data.todos 突然报错 Cannot read property 'map' of null,也不用担心 Cannot read property 'title' of null

# schema
type Query {
  hello: String
  todos: [Todo!]!
}

type Todo {
  id: ID!
  title: String!
}

# query
{
  todos {
    id
    title
  }
}

如果不使用 graphql,你无法保证响应中数据的类型。你可能需要使用 lodash 来做一些边界的处理

const data = {
  todos: [
    {
      id: 1,
      title: "learn react",
    },
  ],
};

// 使用 graphql 后
data.todos.map(todo => todo.title.replace(" ", ""));

// 如果不使用 graphql,你无法保证返回数据类型。你可能需要使用 lodash 来做一些边界的处理
_.map(data.todos, todo => _.replace(todo.title, " ", ""));

当在数据库中,一个字段除了整型,浮点型等基本类型外,还会有更多而且比较重要和常用的数据类型:jsondatetime。既然 scalar 用以表示 field 的数据类型,那么它如何表示 jsondatetime 或者更多的数据类型呢?

这时,可以使用 graphql.js 的API graphql.GraphqlQLScalarType 来自定义 scalar

Mutation

graphql 能够简化一切的查询,或者说它是简化了服务端开发人员 CRUD 中的 Read。那么,如何对资源进行修改呢?这里就提到了 Mutation

# 在后端的 schema
schema {
  query: Query
  mutation: Mutation
}

type Mutation {
  addTodo(title: String!): Todo
}

# 在前端的 query
mutation ADD_TODO {
  addTodo(title: "学习 React") {
    id
    title
  }
}
// 以上示例返回结果
{
  addTodo: { id: 128, title: '学习React' }
}

以上是一个添加 Todo 的例子,从这里可以注意到几点

  1. MutationQuery 同样属于特殊的 object type,同样,所有关于数据的更改操作都要从 Mutation 中找起,也需要放到 schema
  2. MutationQuery 分别为 graphql 的两大类操作,在前端进行 Mutation 查询时,需要添加 mutation 字段 (Query 查询时,在前端添加 query 字段,但这不是必选的)

input type & variables

type Mutation {
  addTodo(title: String!): Todo
}

mutation ADD_TODO {
  addTodo(title: "学习 React") {
    id
    title
  }
}

以上是一个关于添加 Todo 的 mutation,在我们添加一个 Todo 时,它仅有一个属性: title。如果它拥有更多个属性呢?这时,可以使用 input type,把某一资源的所有属性聚合起来。并且配合 variables 一起使用传递数据

input InputTodo {
  title: String!
}

type Mutation {
  addTodo (todo: InputTodo!): Todo
}

mutation ADD_TODO ($todo: InputTodo!) {
  addTodo (todo: $todo) {
    id
    title
  }
}
// $todo 的值,在前端获取数据时,使用 variables 传入
{
  title: "学习 React";
}