深入GraphQL 的使用语法

对于GraphQL 的使用语法在上一节中已经大概介绍了基本的使用方式了,这一篇将会对上一篇入门做拓展,努力将所有的使用语法都覆盖到。

1. 终端语法

首先是介绍在前端查询时用的语法,分成Query 和Mutation 两部分,Subscription 的和Query 是类似的就不特别说明了。

1.1 Query

假设我们现在有一个数据集,结构是这样的:

  1. 学生和老师各有各自的特有字段;
  2. 学生中有个获取所有相关老师的方法,老师中也有一个获取所有学生的方法;
  3. 学生和老师互为多对多关系;
classDiagram
    Student <|-- Teacher2Student
    Teacher <|-- Teacher2Student

    class Student {
        +Int id
        +String name
        +Int age
        +teachers(id_eq: number) get_all_teachers
    }
    class Teacher {
        +Int id
        +String name
        +Boolean gender
        +students(name_contains: string) get_all_students
    }
    class Teacher2Student {
        +Int id
        +Int student_id
        +Int teacher_id
        +student() get_student
        +teacher() get_teacher
    }

首先最简单的使用方式我们可查:

query {
  student {
    id
    name
    teacher {
      id
      name
    }
  }
}

# 结果:

{
  data: {
    student: [
      {
        id: 1,
        name: "张三",
        teacher: [
          {
            id: 1,
            name: "李老师"
          },
          {
            id: 2,
            name: "吴老师"
          }
        ]
      },
      {
        id: 2,
        name: "李四",
        teacher: [
          {
            id: 1,
            name: "李老师"
          }
        ]
      },
      {
        id: 3,
        name: "王三",
        teacher: [
          {
            id: 2,
            name: "吴老师"
          }
        ]
      }
    ]
  }
}

我们通过上面的查询可以得到总共有两个学生,其中李老师同时教了他们两人。这是最最基本的用法。下面我们慢慢加查询需求去改变结果。

1.1.1 参数 Arguments

我们可以通过添加参数的方式限制返回集的内容

query {
  student(name_contains: "三") {  # <-- 这里限制了只要名字中包含了“三”的学生
    id
    name
    teacher(id_eq: 2) {  # <-- 这里限制了只要id=2 的老师
      id
      name
    }
  }
}

# 结果:

{
  data: {
    student: [
      {
        id: 1,
        name: "张三",
        teacher: [
          {
            id: 2,
            name: "吴老师"
          }
        ]
      },
      {
        id: 3,
        name: "王三",
        teacher: [
          {
            id: 2,
            name: "吴老师"
          }
        ]
      }
    ]
  }
}

这时,因为我们过滤了只要id 为2 的老师,所以李老师就给过滤掉了;因为设置了过滤只要名字中带“三”字的学生,所以李四也给过滤掉了。

同理,也可以用参数去对内容进行分页、跳过等操作,操作相同就不写例子了。

1.1.2 别名 Aliases

因为在查询中,不同的数据实体在Graphql 语句中本身是类似于直接请求这个单元的存在,所以如果你同时请求两个相同的集时它就会报错;因为它们都应该是一一对应的。这时你就可以用别名来解决这个问题:

query {
  san: student(name_contains: "三") {
    id
    name
  }
  wang: student(name_contains: "王") {
    id
    name
  }
}

# 结果:

{
  data: {
    san: [
      {
        id: 1,
        name: "张三"
      }
    ]
    wang: [
      {
        id: 3,
        name: "王三"
      }
    ]
  }
}

处理请求结果的时候要注意用了别名的内容是以别名为key 返回的,不是原来的名字了。

1.1.3 片段 Fragments

看上面的查询语句,我们可以看到当用了不同的别名时我们难免会产生这种写了一堆重复的字段名的情况。我们这个例子字段少还好,但正常的业务要有个几十个字段都是挺常见的,这样写可就太费劲了,这时就可以祭出Fragments 来处理了:

fragment studentFields on Student {
    id
    name
}

query {
  san: student(name_contains: "三") {
    ...studentFields
  }
  wang: student(name_contains: "王") {
    ...studentFields
  }
}

# 结果:

{
  data: {
    san: [
      {
        id: 1,
        name: "张三"
      }
    ]
    wang: [
      {
        id: 3,
        name: "王三"
      }
    ]
  }
}
1.1.4 操作名 Operation name

这个相对来说是比较少用的,起码我个人的使用情况来看,我基本更倾向于“能省就省”的原则;但写教程的话就还是介绍下吧。主要出现在同时有多个操作的情况下,用于区分操作数据。

query op1 {
  student(name_contains: "三") {
    id
  }
}

query op2 {
  student(name_contains: "王") {
    id
  }
}

# op1, op2 就是操作名。
# 但日常写query 你甚至可以将操作符("query")也省了像下面这样写就行

{
  student {
    id
  }
}
1.1.5 操作参数 Variables

这个参数有别于上面提到的Arguments, Arguments 是用于具体数据结点操作用的。Variables 指的是面向操作符时,可以让Query 变得可复用,也方便在不同地方使用。

假设我们有两个不同的页面,都要查询学生表但过滤不同,这时如果我们写两个查询像下面这样就很浪费,代码也很丑,也不能复用。

# 页面一用的
query {
  student(name_contains: "三") {
    id
  }
}

# 页面二用的
query {
  student(name_contains: "王") {
    id
  }
}

因为本质上说,它们查询的内容是相同的,只是参数有点不一样,这里我们可以把参数给提取出来,通过在实际使用时再由不同情况传参就好:

# 页面一、二都用同一个Query
query($name: String) {
  student(name_contains: $name) {
    id
  }
}

使用时改变传进去的Variables, 例:

const query = gql`
  query($name: String) {
    student(name_contains: $name) {
      id
    }
  }
`

const page1 = post(URL, query=query, variables={name: "三"})
const page2 = post(URL, query=query, variables={name: "王"})

这样出来的结果就和上面写两个不同的Query 是一样的,但代码会优雅很多,Query 也得到了合理复用。如果有一天需要修改请求的返回结果,也不用跑到各个地方一个一个地修改请求的Query.

注意定义参数有几个硬性规定:

  1. 参数名要以 $ 开头。没得商量不以美元符开头它不认的。
  2. 参数的类型必须和它将会用到的地方的类型一样,否则会出错。因为Graphql 是静态类型的语言。
  3. 可以以类似TS 的方式给参数默认值,如下。
# 这样如果没有给任何参数则$name 会默认等于“三”
query($name: String = "三") {
  student(name_contains: $name) {
    id
  }
}
1.1.6 指示符 Directives

Directives 可以翻译成指示符,但我觉得不太直观,它的功能主要是类似一个条件修饰,类似代码中的if-else 块差不多的功能。让你可以在外面指定要怎么请求的细节。

query($name: String, $withTeacher: Boolean!) {
  student(name_contains: $name) {
    id
    teacher @include(if: $withTeacher) {
      id
    }
  }
}

它的主要作用就是说,如果你在外面的variables 中给定withTeacher=true 那它就会请求teacher 节点,等同于:

query($name: String) {
  student(name_contains: $name) {
    id
    teacher {
      id
    }
  }
}

反之,如果指定withTeacher=false 那它就会省略teacher 节点,等同于:

query($name: String) {
  student(name_contains: $name) {
    id
  }
}

Directives 主要有两个操作符:@include(if: Boolean)@skip(if: Boolean)

这两个的作用相反。另外Directives 这个功能需要服务端有相关支持才能用。但同时,如果需要服务端也可以自已实现完全自定义的Directives.

1.2 Mutation

1.2.1 操作参数 Variables

这个和Query 那边的规则完全一样,参见上面的内容即可,给个小例子:

# 无参写法
mutation create {
  createStudent(name: "王五", age: 18) {
    id
  }
}

# 有参写法
mutation create($name: String, $age: Int) {
  createStudent(name: $name, age: $age) {
    id
  }
}

# 另一种有参写法
# 假设createStudent 函数的参数的类型叫createStudentInput
mutation create($input: createStudentInput!) {
  createStudent($input) {
    id
  }
}
1.2.2 行内片段 Inline Fragments

这里的使用情景主要是针对联合(Union) 类型的,类似于接口(interface) 与类(class)的关系。

假设我们有个接口叫动物(Animal), 有两个类分别是狗(Dog) 和鸟(Bird). 并且我们将这两个类由一个GraphQL 节点给出去:

{
  animal {
    name
    kind
    ... on Dog {
      breed
    }
    ... on Bird {
      wings
    }
  }
}

# 结果

{
  data: {
    animal: [
      {
        name: "Pepe",
        kind: "Dog",
        breed: "Husky"
      },
      {
        name: "Pipi",
        kind: "Bird",
        wings: 2
      }
    ]
  }
}

从上面的结果可以看出,它可以由不同的类型去查不同的“类”,但返回时可以合并返回。就类似于是从一个“接口” 上直接获取到实现类的数据了,非常具体。但大部分情况下我们可能不会合并着查两个不同结构的数据以一个数组返回,我们更多可能是用在于用同一个节点名(animal)就可以查不同的东西但先以他们的类型作了过滤。

1.2.3 元字段 Meta fields

配合上面的例子食用,如果我们没有kind 那个字段时,我们要怎么知道哪个元素是哪个类型呢?我们可以用元字段去知道我们当前操作的是哪个数据实体,主要的元字段有 __typename.

我们可以这样查:

{
  animal {
    name
    __typename
    ... on Dog {
      breed
    }
    ... on Bird {
      wings
    }
  }
}

# 结果

{
  data: {
    animal: [
      {
        name: "Pepe",
        __typename: "Animal__Dog",
        breed: "Husky"
      },
      {
        name: "Pipi",
        __typename: "Animal__Bird",
        wings: 2
      }
    ]
  }
}

__typename 是内置的,你可以在任何节点上查,它都会给你一个类型。

2. 类型定义

2.1 基础

我们知道GraphQL 是一个静态类型的语法系统,那么我们在真正使用前就必须先定义好它的类型。
GraphQL 的类型定义叫做Schemas. 有它自已独立的语法。里面有各个基本类型Scalar,可以定义成不同对象的Type. 也可以自已用基本类型定义成新的类型。

所有的不同的对象最终会组成一个树状的结构,根由schema 组成:

schema {
  query: Query
  mutation: Mutation
}

然后再定义里面一层一层的子对象,比如我们上面那个模型大概可以写成:

type Query {
  student(name_contains: String): Student
  teacher(id_eq: ID): Teacher
}

type Student {
  id: ID!
  name: String!
  age: Int
  teachers: [Teacher!]!
}

type Teacher {
  id: ID!
  name: String!
  gender: Boolean
}

像上面这样我们就定义了两个不同的对象及他们的属性。其中,如果是必填或者说非空的字段则带有"!" 在它的类型后面,比如id: ID! 就表明id 是个非空的字段。非空的字段如果在操作中给它传null 会报错。另外某种类型组成的数组可以用类型加中括号组成,比如上面的Student 里面的Teacher.

定义一个字段为数组:

myField: [String!]

这样定义呢,表明了这个字段本身是可以为 null 的,但它不能有 null 的成员。比如说:

const myField: null // valid
const myField: [] // valid
const myField: ['a', 'b'] // valid
const myField: ['a', null, 'b'] // error

但如果,是这样定义的:

myField: [String]!

则代表它本身不能为 null 但它的组成成员中可以包含 null .

const myField: null // error
const myField: [] // valid
const myField: ['a', 'b'] // valid
const myField: ['a', null, 'b'] // valid

2.2 自带类型

GraphQL 默认的自带类型只有5 种。分别是:

ID: 就类似传统数据库中的ID 字段,主要用于区别不同的对象。可以直接是一个Int, 也可能是一个编码过的唯一值,比如常见的relay 中使用的是“类名:ID” 的字符串再经base64 转码后的结果作为ID. 要注意的是这个ID 只是存在于Graphql 中的。它不一定和数据库中的是对应的。比如relay 这个情况,数据库中存的可能还是一个整数并不是那个字符串。

Int: 整数,可正负。

Float: 双精度浮点数,可正负。

String: UTF-8 字符的字符串。

Boolean: true / false.

如果不能满足你的业务场景你就可以自定义新的类型,或者是找第三方做好的拓展类型。

定义一个类型的Graphql 写法很简单,比如我们新增一个Date 类型。

scalar Date

就这样就可以了,但是你还需要在你的代码中实现它的具体功能,怎么转换出入运行时等等。

另外,Graphql 中支持枚举类型,可以这样定义:

enum GenderTypes {
  MALE
  FEMALE
  OTHERS
}

2.3 接口(Interface) 和联合类型(Union)

Interface 和 Union 很像,所以我就合在一起讲了。

Interface 和其他语言的类似,都是为了给一个通用的父类型定义用的。可以像这样定义及使用:

interface Animal {
  id: ID!
  name: String
}

type Dog implements Animal {
  id: ID!
  name: String
  breed: String
}

type Cat implements Animal {
  id: ID!
  name: String
  color: String
}

可以看到,接口定义的每个字段在实现时都会带上,但它也可以有自已的字段。查询时,需要注意的是:你不可以直接在Animal 上查到各个独有的字段,因为当你在Animal 上做查询时系统并不知道你当前查询的对象是Dog 还是Cat. 你需要用inline fragment 去指定。

# 这样查直接报错:
# "Cannot query field \"color\" on type \"Animal\". Did you mean to use an inline fragment on \"Cat\"?"
query {
  animal {
    id
    name
    color
  }
}

# 正确的打开方式:
query {
  animal {
    id
    name
    ... on Cat {
      color
    }
  }
}

讲完Interface, 我们再看看Union.
Union 你可以直接理解成是没有共同字段的Interface.

union Plant = Lily | Rose | Daisy

查询时和接口一样得用inline fragments 去指定类型。

2.4 输入类型 Input types

上面在那个Mutation 的Variables 举例子时稍微提到过,就是给某个操作的输入的所有参数指定成一个类型,这样可以更方便地添加内容也增加了代码可复用的程度。

假设我们有一个Mutation 的定义是这样的:

type Mutaion {
  createSomething(foo: Int, bar: Float): Something
}

使用Input types:

input CreateSomethingInput {
  foo: Int
  bar: Float
}

type Mutaion {
  createSomething(input: CreateSomethingInput): Something
}

阿兵
7 声望2 粉丝