逐步动手理解GraphQL.

首先

因为最终想要使用AWS AppSync,所以大致浏览了一下GraphQL和AppSync的使用方法,但真正使用过之后才能明白。因此,首先在本地搭建Apollo服务器,尝试手动发送查询并试图理解GraphQL,这是第一步。

请注意,本文不涉及“GraphQL的优点和优点”。我们将重点放在通过运行简单示例来理解使用GraphQL的步骤上。

GraphQL是什么

维基百科对GraphQL(图灵查询语言和运行时)进行了解释。根据其被描述为查询语言和运行时的说明,可以理解它是一个包含多个项目的概念,因此很难用简洁的方式进行解释。为了理解GraphQL,下面的图表展示了相关项目。当提到GraphQL时,这个图表显示了全部或部分内容。

graphql.png

我认为,可以将GraphQL理解为一种提供用户与实际数据之间连接实现的方式,以高效地获取(和修改)数据。

和一般的的数据库服务(关系型数据库管理系统,如Oracle Database或MySQL)相比,关系型数据库管理系统(RDBMS)涵盖了右侧的黄色框并采用了服务独有的规范来存储数据。可以通过SQL和DDL来操作数据库和管理数据。从用户和数据库管理员的角度来看,需要编写SQL或其他代码来操作数据,但不需要编写针对已写代码的具体数据操作程序。

然而,在GraphQL服务中,可以将这两个过程分开。代替的是,需要以resolver的形式实现一个程序,以确定对外部给定的命令要返回什么处理过的数据。

在本文中,我们将使用Apollo作为GraphQL服务。虽然在说明中将数据分离,但是只要在GraphQL服务的解析器中实现返回固定值或者服务器内存中的值,就可以不创建数据集部分即可使GraphQL服务运行。

使用的环境和版本等

    • OS: Ubuntu 20.04 LTS

 

    • Node.js: v15.8.0

 

    • yarn: 1.22.10

 

    • apollo-server: 2.25.2

 

    graphql: 15.5.1

阿波罗设置和你好,世界

最开始,可以参考 Github 的 README 进行设置。

$ cd [プロジェクトディレクトリ]
$ yarn init
# ... 手を動かすだけなので今回は yarn init の入力内容は適当に
$ yarn add apollo-server graphql
# サーバーサイドスクリプトを記載
$ vim index.js

将index.js文件中的内容按原样保存在README文件中。

const { ApolloServer, gql } = require('apollo-server');

// The GraphQL schema
const typeDefs = gql`
  type Query {
    "A simple type for getting started!"
    hello: String
  }
`;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`? Server ready at ${url}`);
});

在写完这句话后,启动本地服务器,并访问 http://localhost:4000。

$ node index.js

然后,会出现如下的控制台。

SnapCrab_NoName_2021-7-7_17-15-45_No-00.png

在这里,您可以在左侧输入GraphQL查询并按下中央的执行按钮,然后结果将显示在右侧。在这里,我们可以尝试发送查询并接收来自GraphQL的响应。

query {
  hello
}

当下指定您的查询时,

{
  "data": {
    "hello": "world"
  }
}

会有如下回应返回。

这样一来,首先的步骤已经完成了。

虽然在这里写着 “query { hello } “,但根据下面所述的条件,当满足条件时,可以省略 query(在下文中将采用适时省略)。

只有当GraphQL文档定义了多个操作时,才需要指定查询关键字和操作名称。因此,我们可以使用查询简写来书写上述查询:https://github.com/graphql/graphql-spec/blob/main/README.md

GraphQL 的增删改查操作

在添加数据之后执行查询操作

请修改index.js,以便能够获取简单的用户数据。

// The GraphQL schema
const typeDefs = gql`
  type User {
    name: String!
    age: Int!
  }
  type Query {
    hello: String
    user(name: String!): User
    users: [User]
  }
`;

const users = [
  { name: 'Sample User1', age: 20 },
  { name: 'Sample User2', age: 30 },
  { name: 'Sample User3', age: 40 },
]

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
    user: (parent, args, context, info) => { 
      return users.find(u => u.name === args.name);
    },
    users: () => users,
  },
};

通过类型定义(typeDefs),定义了一个名为User的数据类型,并且使得可以获取User类型的值。在这里使用了gql,并且据说这种语法是完全没见过的,叫做”带标签的模板字面量”。
从机制上来说,可以通过fx`args`这种形式来调用函数fx,而不是fx(args)。然而,后半部分的`args`是模板字符串。因此,只能在传递字符串时使用,比如fx `my name is ${name}`。更详细的说明请参考以下内容。

我們實際執行這個查詢。

{
  hello
}
{
  "data": {
    "hello": "world"
  }
}
{
  users {
    name, age
  }
}
{
  "data": {
    "users": [
      {
        "name": "Sample User1",
        "age": 20
      },
      {
        "name": "Sample User2",
        "age": 30
      },
      {
        "name": "Sample User3",
        "age": 40
      }
    ]
  }
}
{
  user(name: "Sample User1") {
    age
  }
}
{
  "data": {
    "user": {
      "age": 20
    }
  }
}

就像这样,在新定义的 type Query 中可以使用定义的 users 和 user。

顺便说一句,对于熟悉SQL的人来说,当想要获取User的所有项目时,很可能不想逐一列出字段。因此,他们可能会想要执行以下查询。

# 全ユーザーの全属性を取得したい
{
  users
}
# 全ユーザーの全属性を取得したい
{
  users { * }
}

然而,这些查询不会返回预期的值。
对于示例1,会返回错误消息”类型为\”[User]\”的字段\”users\”必须有子字段的选择。您是不是想要\”users { … }\”?”。根据错误消息的说明,在查询具有值(非原始值)的子元素(属性)时,需要明确指定所需的属性(hello返回的是原始字符串,所以{ hello }是有效的)。

如果示例1失败,有些人可能会考虑像示例2一样使用通配符来简化表示。然而,这将导致语法错误。据说,在GraphQL实现中,尽管有关通配符的讨论正在进行,但目前没有计划引入。

通过使用所述的Fragment可以在一定程度上实现共通化,但即使如此,也仍然需要至少一次地记录所需项。

获取有条件的数据

在之前的例子3中,我们获取了与名称匹配的用户。当然,这也是有条件的数据获取(获取满足名称完全匹配的用户)但是,对于具有更复杂条件的数据,比如“获取年龄在某个范围内的用户”,应该怎么办呢?
最简单的方法是“在用户端获取所有数据,然后在用户端进行筛选并使用所需数据”,但是这会增加数据传输量,所以不是很理想。
因此,在这种情况下,我们会采取创建用于搜索条件的数据结构等方法。
这里,输入的UserAgeFilter将成为用于搜索的数据结构。

像 UserAgeFilter 这样通过 type 定义的是输出类型定义,而通过 input 定义的是输入类型定义。input 可以作为 Query 的参数使用,但 type 不可以。

// The GraphQL schema
const typeDefs = gql`
  input UserAgeFilter {
    gt: Int
    lt: Int
  }
  type User {
    name: String!
    age: Int!
  }
  type Query {
    hello: String
    users(name: String, ageFilter: UserAgeFilter): [User]
  }
`;

const users = [
  { name: 'Sample User1', age: 20 },
  { name: 'Sample User2', age: 30 },
  { name: 'Sample User3', age: 40 },
]

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
    users: (parent, args, context, info) => { 
      let us = users;
      if (args.name) { 
          us = us.filter(u => u.name === args.name);
      }
      if (args.ageFilter) {
        if (typeof args.ageFilter.gt !== 'undefined') {
          us = us.filter(u => u.age > args.ageFilter.gt);
        }
        if (typeof args.ageFilter.lt !== 'undefined') {
          us = us.filter(u => u.age < args.ageFilter.lt);
        }
      }
      return us;
    },
  },
};

通过查看代码,可以理解使用了传递给 users 的参数 name 和 ageFilter 来进行数据过滤。以下是 users 的使用示例。

{
  users (ageFilter: {gt: 29}) {
    name, age
  }
}

# 取得結果
# {
#   "data": {
#     "users": [
#       {
#         "name": "Sample User2",
#         "age": 30
#       },
#       {
#         "name": "Sample User3",
#         "age": 40
#       }
#     ]
#   }
# }
{
  users (name: "Sample User1", ageFilter: {gt: 19, lt: 38}) {
    name, age
  }
}

# 取得結果
# {
#   "data": {
#     "users": [
#       {
#         "name": "Sample User1",
#         "age": 20
#       }
#     ]
#   }
# }

最終的结论是要根据输入实现一个解析器,它会获取并返回适当的数据。这里只实现了简单的 lt 和 gt,但也可以考虑实现 lte 和 gte,以及通过传递字符串进行解析,并根据结果应用过滤器的通用实现。

数据的修改(创建/更新/删除)

我们过去一直关注于数据的获取 (Read),但是我们现在要研究一下除了 CRUD 操作中的 Read 之外的数据生成、更新和删除该如何实现。

使用GraphQL的mutation来进行数据的更改。在这里,我们将实现一个名为add的Mutation,用于添加数据。

// The GraphQL schema
const typeDefs = gql`
  type User {
    name: String!
    age: Int!
  }
  type Query {
    users: [User]
  }
  type Mutation {
    add(name: String!, age: Int!): String
  }
`;

const users = [
  { name: 'Sample User1', age: 20 },
  { name: 'Sample User2', age: 30 },
  { name: 'Sample User3', age: 40 },
]

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    users: (parent, args, context, info) => { 
      return users;
    },
  },
  Mutation: {
    add: (parent, args, context) => {
      const u = { name: args.name, age: args.age };
      users.push(u);
      return `added ${u.name}`;
    }
  }
};

如果需要添加mutation,它的调用方法如下所示。前面已经解释过,如果是查询(query)的情况可以省略,但是mutation不能省略,所以询问语句必须从mutation开始。

mutation {
  add(name: "Hoge", age: 10)
}
{
  "data": {
    "add": "added Hoge"
  }
}

在获取用户时,数据的末尾会添加一个名为{name: “Hoge”, age: 10}的用户。

query {
  users {
    name, age
  }
}

# {
#   "data": {
#     "users": [
#       {
#         "name": "Sample User1",
#         "age": 20
#       },
#       {
#         "name": "Sample User2",
#         "age": 30
#       },
#       {
#         "name": "Sample User3",
#         "age": 40
#       },
#       {
#         "name": "Hoge",
#         "age": 10
#       }
#     ]
#   }
# }

在本次的实现中,用户数据没有被持久化,所以如果重新启动应用程序,新增的数据会恢复到原始状态。但是,如果将这些数据保存并永久化在外部文件或数据库中,即使重新启动应用程序,新增的数据也会继续保留下来。

无论是更新还是删除,只需通过不同的解析器实现变异,方法都是相同的。

获得有关发展数据的方法

在这里,我们将看一下实际上可能遇到的稍微复杂的GraphQL查询会怎样发展。

使用一次查询获取多个数据

如果在查询中列举了多个查询项,可以在一次请求中获取它们。

query {
  hello,
  users { name },
  users { age }
}
{
  "data": {
    "hello": "world",
    "users": [
      {
        "name": "Sample User1",
        "age": 20
      },
      {
        "name": "Sample User2",
        "age": 30
      },
      {
        "name": "Sample User3",
        "age": 40
      }
    ]
  }
}

指定返回值字段的名称 (别名)

到目前为止,返回值的结果被设置在名为 data 的对象内,与查询名称相同的字段中。然而,在GraphQL中,调用方可以指定字段名。在GraphQL的上下文中,这被称为别名。以下是具体的用法示例。

{
  names: users { AreYou: name },
  ages: users { Age: age }
}
{
  "data": {
    "names": [
      {
        "AreYou": "Sample User1"
      },
      {
        "AreYou": "Sample User2"
      },
      {
        "AreYou": "Sample User3"
      }
    ],
    "ages": [
      {
        "Age": 20
      },
      {
        "Age": 30
      },
      {
        "Age": 40
      }
    ]
  }
}

可以使用相同的方法,给字段内部的值添加另一个别名。

当有一个名为findUser的查询时,作为一种方法,用户可以设置一个方便的名称,如loginUser: findUser(…),以增加用户在命名方面的自由。

获取相同类型的值 (片段)

在这里,我们考虑以下种类的查询。

{
  user: user (name: "Sample User1") { name, age },
  bestFriend: user(name: "Sample User2") {name, age}
}
# {
#   "data": {
#     "user": {
#       "name": "Sample User1",
#       "age": 20
#     },
#     "bestFriend": {
#       "name": "Sample User2",
#       "age": 30
#     }
#   }
# }

在这里,我们从同一个解析器(resolver)中获取值。在返回值的指定中,我们写了name和age。虽然目前只有两个字段,可能还可以接受,但如果字段和项目增加了,我们无法一眼看出是否尝试获取相同的字段,而且如果有修改的话会变得很麻烦。

作为这种情况下的常见解决方法,有一种叫做Fragment的方法。上述查询可以被替换为以下方式:

{
  user: user (name: "Sample User1") { ...userField },
  bestFriend: user(name: "Sample User2") { ...userField }
}

fragment userField on User {
  name, age
}

查询变量和变量展开

迄今为止,在调用查询时,我们一直发送包含具体值的查询语句(如:user(name: “Sample User1”))。然而,如果我们试图基于用户的输入简单地拼接查询语句来自客户端应用程序调用查询,则会出现以下问题。

    • 入力次第でクエリが成立しなくなる

 

    入力内容により既存の構文を改ざんし 関係ないクエリ/ミューテーションが発行できる可能性がある (=SQL Injectionと同じようなことが起こる)

在GraphQL中,有一种将查询分解为“语法”和“变量”的解决方法。在语法中,定义以$开头的变量,在执行时会将传递的变量进行展开。对于熟悉SQL的人来说,可以将其理解为占位符语法(例如,在变量位置插入$?等,将展开参数作为SQL模板的额外参数传递给函数执行的机制),这样可能更容易理解。

如果使用Apollo Server,可以在屏幕左下角输入查询变量,并且需要用JSON格式输入。

SnapCrab_NoName_2021-9-5_18-8-58_No-00.png

重新輸入先前在控制台中所寫的內容。

query ($name: String!){
  user(name: $name) {
    name, age
  }
}
{
  "name": "Sample User1"
}
{
  "data": {
    "user": {
      "name": "Sample User1",
      "age": 20
    }
  }
}

关于数据类型的递归定义

在GraphQL中,无法递归定义类型。因此,例如无法在类型中创建循环依赖,如下面的friends。

type User {
    name: String!
    age: Int!
    friends: [User]
}

概括起来

我实现了Apollo的解析器,并解释了执行CRUD操作的实现方法和实际语法(Fragrment,Variables)时发出的查询。通过实际操作,我认为我已经理解了基本的GraphQL使用和实现方法。

bannerAds