使用Node.js和MongoDB构建的,学习GraphQL的过程
首先
由于我在维护和修改阶段加入了项目,所以没有做过基本的操作,如创建架构的工作。另外,我还没有使用过NoSQL数据库,所以我决定学习一下,尝试用Node.js+MongoDB来建立GraphQL服务器。
这次创建的项目已经上传到GitHub上了。
GraphQL的好处
GraphQL最大的特点在于它是一个单一的终端点,因此与REST进行比较的文章到处都有(例如, 这里)。
使用GraphQL可以解决在尝试使用REST获取所有需要的数据时可能遇到的以下问题。
-
- 複数のエンドポイントへリクエストを行う必要がある
- 不要なデータも一緒に取得されてしまう
服务器构建
创建一个适当的文件夹(graphql-server-practice)并运行yarn init。然后按照以下配置创建文件夹和文件。
我们将在Schema.js中记录Schema、Query和Mutation。
创建模式
为了练习,我们将创建三个模式:User、Hobby和Post。
User与多个Hobby和Post之间形成了一对多的关系。
首先,通过运行”yarn add graphql”命令安装graphql包,然后在schema.js文件中加载以下内容。
const graphql = require('graphql');
const {
GraphQLObjectType,
GraphQLID,
GraphQLString,
GraphQLInt,
GraphQLSchema,
GraphQLNonNull,
GraphQLList,
} = graphql;
用UserType作为User的模式创建。通过GraphQLObjectType进行封装,可以定义name、description和fields的模式。
在GraphQL中,我们将fields的id、name、age和profession定义为GraphQLID、GraphQLString、GraphQLInt和GraphQLString这些标量类型。
const UserType = new GraphQLObjectType({
name: 'User',
description: 'Documentation for user...',
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
age: { type: GraphQLInt },
profession: { type: GraphQLString },
}),
});
同样地,我们也将为HobbyType和PostType创建相应的选项。
const HobbyType = new GraphQLObjectType({
name: 'Hobby',
description: 'Hobby description',
fields: () => ({
id: { type: GraphQLID },
title: { type: GraphQLString },
description: { type: GraphQLString },
}),
});
const PostType = new GraphQLObjectType({
name: 'Post',
description: 'Post description',
fields: () => ({
id: { type: GraphQLID },
comment: { type: GraphQLString },
}),
});
我已经创建了3个Schema,但是在目前的状态下,User和Post以及User和Hobby之间的关系还没有被定义,所以我们需要考虑这些关系。
让我们来思考一下用户和帖子的例子。
const UserType = new GraphQLObjectType({
name: 'User',
description: 'Documentation for user...',
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
age: { type: GraphQLInt },
profession: { type: GraphQLString },
posts: {
type: new GraphQLList(PostType),
resolve(parent, args) {
return postsData.filter((data) => data.userId === parent.id);
},
},
}),
});
const PostType = new GraphQLObjectType({
name: 'Post',
description: 'Post description',
fields: () => ({
id: { type: GraphQLID },
comment: { type: GraphQLString },
user: {
type: UserType,
resolve(parent, args) {
return usersData.find((data) => data.id === parent.userId);
},
},
}),
});
对于每个User,都存在多个Post。因此,在UserType的字段中,新创建的posts将成为PostType的数组类型,并被定义为new GraphQLList(PostType)。
posts: {
type: new GraphQLList(PostType),
resolve(parent, args) {
return postsData.filter((data) => data.userId === parent.id);
},
},
另外,resolve会定义要显示给哪个用户的帖子。parent指的是父级字段(在这里是UserType),在以上操作中它只会获取具有与用户id相等的userId的帖子数据。
const usersData = [
{ id: '1', name: '山田勝己', age: 36, profession: 'SASUKE' },
];
const postsData = [
{ id: '1', comment: '僕にはSASUKEしかないんです', userId: '1' },
{ id: '2', comment: '完全制覇がしたいんです', userId: '1' },
];
创建查询
在GraphQL中,查询和Schema一样,使用GraphQLObjectType进行定义。让我们试着创建一个user查询,用于获取指定id的用户,并且创建一个users查询,用于获取所有用户。
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
description: 'Description',
fields: {
user: {
type: UserType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
return usersData.find((data) => data.id === args.id);
},
},
users: {
type: new GraphQLList(UserType),
resolve(parent, args) {
return usersData;
},
},
},
});
用户查询中使用id作为参数(args),因此在fields中对args进行类型定义。在resolve中,编写了一个处理程序,仅获取与args的id相同的数据。
在users查询中,我们只需要获取所有数据,所以不需要args参数。
设置本地服务器并进行查询操作的确认。
在进行变异构建和与数据库的连接之前,您需要启动本地服务器以确认查询的操作。
将创建的 RootQuery 用 new GraphQLSchema 进行包装并导出。
module.exports = new GraphQLSchema({
query: RootQuery
});
使用yarn命令添加express express-graphql所需的软件包,并进行以下设置。
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const schema = require('./schema/schema');
const app = express();
app.use(
'/graphql',
graphqlHTTP({
graphiql: true,
schema,
})
);
app.listen(4000, () => {
console.log('Listening for requests on my awesome port 4000');
});
通过在应用程序中使用 `node app` 命令,可以在4000端口上启动服务器。为了实时反映源代码的修改,我们将使用 `yarn global add nodemon` 命令来引入 nodemon。
运行nodemon app来启动服务器,打开http://localhost:4000/graphql,会出现以下画面。
与MongoDB的连接
与DB建立连接并实现Mutation操作。
MongoDB Atlas的设定
Node.js 的配置
为了与GraphQL和MongoDB进行集成,我们将使用Node.js的mongoose包进行安装(yarn add mongoose)。
将app.js文件修改为以下内容。使用mongoose.connect连接到MongoDB,使用mongoose.connection.once在成功连接后进行控制台日志确认。
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const mongoose = require('mongoose');
const schema = require('./schema/schema');
const app = express();
mongoose.connect(
'mongodb+srv://dbUser:<password>@cluster0.gjo5x.mongodb.net/test', // <password>には自分で設定したものを入力
{ useNewUrlParser: true }
);
mongoose.connection.once('open', () => {
console.log('we are connected.');
});
app.use(
'/graphql',
graphqlHTTP({
graphiql: true,
schema,
})
);
app.listen(4000, () => {
console.log('Listening for requests on my awesome port 4000');
});
建立模型
用户的模型将如下所示。userSchema将在稍后的schema.js文件中加载,最后进行导出。
const mongoose = require('mongoose');
const MSchema = mongoose.Schema;
const userSchema = new MSchema({
name: String,
age: Number,
profession: String,
});
module.exports = mongoose.model('User', userSchema);
产生变异
我们将创建用户(User)数据的创建(CreateUser)、更新(UpdateUser)、删除(RemoveUser)的Mutation。
const graphql = require('graphql');
const User = require('../model/user');
~中略~
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
CreateUser: {
type: UserType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
age: { type: new GraphQLNonNull(GraphQLInt) },
profession: { type: GraphQLString },
},
resolve(parent, args) {
let user = new User({
name: args.name,
age: args.age,
profession: args.profession,
});
return user.save();
},
},
UpdateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
name: { type: new GraphQLNonNull(GraphQLString) },
age: { type: GraphQLInt },
profession: { type: GraphQLString },
},
resolve(parent, args) {
return (updatedUser = User.findByIdAndUpdate(
args.id,
{
$set: {
name: args.name,
age: args.age,
profession: args.profession,
},
},
{ new: true }
));
},
},
RemoveUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
},
resolve(parent, args) {
let removedUser = User.findByIdAndRemove(args.id).exec();
if (!removedUser) {
throw new 'Error'();
}
return removedUser;
},
},
}
~中略~
module.exports = new GraphQLSchema({
query: RootQuery,
mutation: Mutation,
});
在resolve中加载User模型,并使用save方法和findByIdAndUpdate方法进行数据的注册和更新。有关这些方法的详细使用说明,请参考mongoose的官方文档。
另外,在更新或删除操作中需指定所要处理的数据,因此args中的id必须为非空值。若要使某一列变为非空值,请使用GraphQLNonNull进行封装。
最后还要记得导出Mutation。
执行突变。
最后
这次的服务器建设工作花费了相当长的时间,但使用AWS AppSync只需一瞬间就建设完成了。这是相当令人震撼的体验,所以我打算在另一篇文章中写一下。此外,如果您对未提及的Post、Hobby的Model和Mutation等部分感兴趣的话,我建议您可以查看一下GitHub。