GraphQL系列二 数据类型

GraphQL是应用层查询语言,它有自己的数据类型,用来描述数据,比如标量类型、集合类型、Object类型、枚举等,甚至有接口类型。而GraphQL服务端不依赖任何语言,因此本篇只是从概念上来了解其数据类型。
GraphQL

标量类型(Scala)

标量类型是不能包含子字段,主要有如下类型:

  • Int: 有符号的32位整型
  • Float: 有符号的双精度浮点型
  • String: UTF‐8字符序列
  • Boolean: 布尔型
  • ID: 常用于获取数据的唯一标志,或缓存的键值,它也会被序列化为String,但可读性差。

以上的类型是GraphQL规范的基本类型,每个规范的实现也可以有自定以标量,如 scala Date ,但它必须能够序列和反序列化,至于如何定于取决于具体的规范实现。

枚举(Enum)

枚举类型是标量类型的变体,不仅适用于可验证性,还提高了维护性,它同样被序列化为String。定义形如

enum Unit {
MM
mm
}

MM代表米做单位,mm代码毫米做单位。

对象(Ojbect)

组成Schema最常用的是对象类型,它包含各种字段,定义如:

type User{
  name: String
  sex: String
  intro: String
}

以上定义了一个User对象,包含name(名字)、sex(性别)、intro(介绍)属性字段,而这些属性字段都是标量String类型,当然属性也可以是对象类型。

集合(List)

GraphQL规范中的集合只有List一种,它是有序的用 [] 表示,如

type User{
name: String
sex: String
intro: String
skills: [String]
}

skills(技能)就是一个list集合,其实更像是一个数组。

非空/Null(Non-Null)

在类型后使用 ! 声明 非空/Null,如

type Query {
user(id:Int!):User
}
type User{
name: String!
sex: String
intro: String
skills: [String]!
}

以上定义了查询接口user,其中参数id为必须提供,如果你使用如下查询语句

query{
user {
name
sex
intro
}
}

则会反馈异常

{
"errors": [
{
"message": "Field \"user\" argument \"id\" of type \"Int!\" is required but not provided.",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}

而实体类型User的name为非null,一但服务端返回的数据name为null,则反馈为

{
"data": {
"user": null
},
"errors": [
{
"message": "Cannot return null for non-nullable field User.name.",
"locations": [
{
"line": 3,
"column": 5
}
],
"path": [
"user",
"name"
]
}
]
}

然而需要注意区别 skills: [String]!skills: [String!] ,前者是集合不能为null,后者是集合元素不能为null。

模式(Schema)

GraphQL的Schema用于生成文档、格式定义与校验等,除了自定义的类型如上文中的User,还有两个特殊的类型Query(查询)和Mutation(维护)。如增查改删(CRUD),增改删属于后者,查属于前者。

schema {
query: Query
mutation: Mutation
}

一个Schema可以没有mutaion,但必须有query。
可以使用操作名为”__schema”和”__type”来查看schema和类型信息,签名如下

__schema: __Schema!
__type(name: String!): __Type

详细的字段如

type __Schema {
types: [__Type!]!
queryType: __Type!
mutationType: __Type
subscriptionType: __Type
directives: [__Directive!]!
}
type __Type {
kind: __TypeKind!
name: String
description: String
# OBJECT and INTERFACE only
fields(includeDeprecated: Boolean = false): [__Field!]
# OBJECT only
interfaces: [__Type!]
# INTERFACE and UNION only
possibleTypes: [__Type!]
# ENUM only
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
# INPUT_OBJECT only
inputFields: [__InputValue!]
# NON_NULL and LIST only
ofType: __Type
}
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
type __InputValue {
name: String!
description: String
type: __Type!
defaultValue: String
}
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
enum __TypeKind {
SCALAR
OBJECT
INTERFACE
UNION
ENUM
INPUT_OBJECT
LIST
NON_NULL
}
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
}
enum __DirectiveLocation {
QUERY
MUTATION
SUBSCRIPTION
FIELD
FRAGMENT_DEFINITION
FRAGMENT_SPREAD
INLINE_FRAGMENT
}

查询(Query)

Query目的是获取数据。格式

query queryName{
operation
}


query userQuery{
user(id:0){
name
sex
stature
}
users{
name
sex
stature
}
}

实际上如果只有一个查询,“query”“queryName”都可以省略,如

{
user(id:0){
name
sex
stature
}
users{
name
sex
stature
}
}

维护(Mutation)

Mutataion是用来维护数据的,格式和查询类似

mutation mutationName{
operation
}


mutation{
addUser(name:"testUser",sex:"男",intro:"简介",skills:[]){
name
sex
intro
}
}

由于Schema可以没有Mutation,但必须有Query,因此mutaion关键字是不可以省略的,否则被认为是Query而找不到操作名。

参数(Argument)

客户端的查询语句中对象和字段都可以传入参数,方便精确控制服务返回结果,查询语句如

{
user(id:1) {
name
sex
intro
skills
stature(unit:MM)
}
}

服务返回数据

{
"data": {
"user": {
"name": "James",
"sex": "男",
"intro": "zhaiqianfeng的英文名",
"skills": [
"Linux",
"Java",
"nodeJs",
"前端"
],
"stature": 1.8
}
}
}

服务端是靠resolver来解析实现的,后文描述。

指令(Directive)

客户端可以使用两种指令skip和include,可以用与字段(field)、片段(fragment),格式和含义如下:

  • field/fragment @skip(if: $isTrue) 当$isTrue为真(true)时不查询field或不使用fragment;
  • field/fragment @include(if: $isTrue) 当$isTrue为真(true)时查询field或使用fragment;

而参数变量是通过query或mutation传递的;变量形如$withName:Boolean!,以$开头,以类型结尾,类型必须是标量(scalar)、枚举(enum)或输入类型(input)。如query为

query(
$noWithDog:Boolean!,
$withName:Boolean!,
$withFish:Boolean!
){
animals{
name @include(if:$withName)
... dogQuery @skip(if:$noWithDog)
... on Fish @include(if:$withFish){
tailColor
}
}
}
fragment dogQuery on Dog{
legs
}

在query定义中分别定义了$noWithDog(是否带着狗狗)、$withName(是否带着Name)、$withFish(是否带着鱼儿)变量,传入的参数变量为

{
"noWithDog": true,
"withName": true,
"withFish": true
}

查询的结果如

{
"data": {
"animals": [
{
"name": "dog"
},
{
"name": "fish",
"tailColor": "red"
}
]
}
}

至于如何传参数,将在下一篇博文中演示。

如果参数变量传给非空的字段,那么参数变量也必须是非空类型,否则可以允许为空,允许为空是属于可选参数。而且参数变量可以有默认值值,如

query Animal($type: String = "dog") {
animal(type: $type) {
name
}
}

输入(Input)

上面的参数(Argument)我们使用是标量,但当使用Mutation来更新数据时,你可能更喜欢传入一个复杂的实体Object,GraphQL使用input关键字来到定义输入类型,不直接用Object是为了避开循环引用、接口或联合等一些不可控的麻烦,特别是input类型不能像Object那样带参数的。如

input UserInput {
name: String!
sex: String
intro: String
skills: [String]!
}

定义了一个输入类型UserInput,字段有name、sex、intro和skills.

接口(Interface)

类似其他语言,GraphQL也有接口的概念,方便查询时返回统一类型,接口是抽象的数据类型,因此只有接口的实现才有意义,如

interface Animal{
  name: String!
}
type Dog implements Animal{
  name: String!
  legs: Int!
}
type Fish implements Animal{
  name: String!
  tailColor: String!
}

上面的接口Animal有两个实现:Dog和Fish,它们都有公共字段name。服务端的查询Schema可以是类似如下的定义

type Query {
animals:[Animal]!
}

而客户端的查询需要使用下面的Fragment,稍后展示。

联合(Union)

联合查询,类似接口式的组合,但不要求有公共字段,使用union关键字来定义,如

type Dog{
  chinaName: String!
  legs: Int!
}
type Fish{
  englishName: String!
  tailColor: String!
}
union Animal = Dog | Fish

则Animal可以是Dog也可以是Fish,服务端定义查询可以和上面的接口相同,客户端查询也需要使用Fragment,稍后展示。

片段(Fragment)

Fragment分为内联和外联,两种都是用于选择主体,而后者还可以共用代码。如上面的接口示例,Dog和Fish都是接口Animal的实现,有接口的公共字段name,简单的内联

{
animals{
name
... on Dog{
legs
}
... on Fish{
tailColor
}
}
animalSearch(text:"dog"){
name
... on Dog{
legs
}
... on Fish{
tailColor
}
}
}

如果是Dog则查询legs字段,如果是Fish则查询tailColor字段,这种内联如果单个查询则比较方便,但多个查询则使用外联更简洁,如下

{
animals{
... animalName
... dogLegs
... fishTail
}
animalSearch(text:"dog"){
... animalName
... dogLegs
... fishTail
}
}
fragment animalName on Animal {
name
}
fragment dogLegs on Dog{
legs
}
fragment fishTail on Fish{
tailColor
}

在联合(union)查询,因为没有公共字段,使用fragment示例如下

{
animals{
... on Dog{
chinaName
legs
}
... on Fish{
englishName
tailColor
}
}
}

别名(Alias)

GraphQL支持别名命名,这对于查询多个雷同的结果非常有用,格式为 aliasName: orginName

{
james:user(id:1) {
name
sex
intro
skills
MM:stature(unit:MM)
mm:stature(unit:mm)
}
zhaiqianfeng:user(id:0) {
name
sex
intro
skills
MM:stature(unit:MM)
mm:stature(unit:mm)
}
}

以上分别把id为1、2的分别给予别名为james、zhaiqianfeng,以米(MM)、毫米(mm)为单位的身高分别给予别名MM、mm,服务端返回的数据如

{
"data": {
"james": {
"name": "James",
"sex": "男",
"intro": "zhaiqianfeng的英文名",
"skills": [
"Linux",
"Java",
"nodeJs",
"前端"
],
"MM": 1.8,
"mm": 180
},
"zhaiqianfeng": {
"name": "zhaiqianfeng",
"sex": "男",
"intro": "博主,专注于Linux,Java,nodeJs,Web前端:Html5,JavaScript,CSS3",
"skills": [
"Linux",
"Java",
"nodeJs",
"前端"
],
"MM": 1.8,
"mm": 180
}
}
}

解析(resolve)

GrapQL API有一个入口点,通常称为“Root”,而查询都是由Root开始,如服务端定义一个root

var root= {
user: {
name: 'zhaiqianfeng',
sex: '男',
intro: '博主,专注于Linux,Java,nodeJs,Web前端:Html5,JavaScript,CSS3'
}
};

在客户端就可以使用如下查询语句

{
user {
name
sex
intro
}
}

其实常用的是使用resolver,resolver是一个函数,用来处理数据,GraphQL规范中resolver的原型是 function(obj,args,context) :

  • obj 代表上一个对象,一般不常用;
  • args 传入的参数;
  • context 上下文,比如session、数据库链接等;

比如express-graphql的是 function(args,context) ,省略了第一个obj。因此上例中你可以使用resolver来处理,比如:

var root= {
user: function(args,context){
return {
name: 'zhaiqianfeng',
sex: '男',
intro: '博主,专注于Linux,Java,nodeJs,Web前端:Html5,JavaScript,CSS3'
}
}
};

翟前锋 wechat
欢迎订阅我的微信公众号:zhaiqianfeng!