用了GraphQL之后,就不想再用RESTful了。

本文将使用Node.js+Docker+GraphQL+MongoDB构建一个具有CRUD功能的完整微服务。

运行代码,需要安装docker和docker-compose。

完整代码见: leinue/node-mongodb-graphql-docker

注:阅读本文需要有GraphQL基础。

Docker

dockerfile

FROM node

WORKDIR /app

EXPOSE 5555

从node构建,将工作目录设置为/app,暴露5555端口

docker-compose

version: "3"
services:
  user_service:
    build: .
    links: 
      - user_db
    command: ["node", "index.js", ' && /bin/bash']
    hostname: user_service_in_container
    volumes:
      - ./:/app
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    ports:
      - "5555:5555"
  user_db:
    image: mongo
    volumes:
      - "/tmp/db:/data/db"
    restart: always

声明了以下任务:

  1. 声明user_service服务和user_db服务
  2. user_service:
    1. 从当前目录的dockerfile构建
    2. 与user_db连接
    3. 在启动时执行node index.js和/bin/bash
    4. 挂载当前目录到/app目录下
    5. 复制一份
    6. 在失败时重启
    7. 暴露容器内的5555端口到宿主机的5555端口
  3. user_db
    1. 从mongo构建
    2. 将容器内的/data/db挂载到宿主机上的/tmp/db上(数据持久化)
    3. 总是重启

运行命令

docker-compose up -d

实现代码

使用babel编译为ES6代码

事实上现在的node v8已经支持大部分ES6代码了,但是async仍不支持,为了使用async不得不使用babel编译。

.babelrc

{
    "presets": ["es2015"],
    "plugins": ["syntax-async-generators", "transform-async-generator-functions", "transform-regenerator"]
}

需要的babel包

{
    "babel-plugin-syntax-async-generators": "^6.13.0",
    "babel-plugin-transform-async-generator-functions": "^6.24.1",
    "babel-plugin-transform-regenerator": "^6.26.0"
    "babel-core": "^6.26.0",
    "babel-polyfill": "^6.26.0",
    "babel-preset-es2015": "^6.24.1",
}

安装之后创建index.js

require('babel-core/register');
require("babel-polyfill");
require('./server.js');

这样就可以在server.js中使用es6的代码了。

server.js

server.js用来初始化graphql服务器、路由和连接mongodb。这里使用koa和graphql服务端的插件来初始化。

import koa from 'koa'; // koa@2
import koaRouter from 'koa-router'; // koa-router@next
import koaBody from 'koa-bodyparser'; // koa-bodyparser@next
import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa';
import cors from 'koa-cors';
import convert from 'koa-convert';
import configs from './configs';

import mongoose from 'mongoose';

const app = new koa();
const router = new koaRouter();

const db = mongoose.createConnection(['mongodb://', configs.mongodb.ip, '/', configs.mongodb.dbname].join(''));

if(db) {
	console.log('mongodb connected successfully');
	global.db = db;
}else {
	console.log('mongodb connected failed');
}

import schemaRouters from './routers/schemaRouters';

const schemas = schemaRouters().default;

router.post('/graphql', koaBody(), graphqlKoa({ schema: schemas.HelloSchema }));
router.get('/graphql', graphqlKoa({ schema: schemas.HelloSchema }));

router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' }));

app.use(convert(cors(configs.cors)));
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(configs.port, () => {
	console.log('app started successfully, listening on port ' + configs.port);
});

这段初始化代码将/graphql作为graphql数据收发的路由地址,服务将启动在5555端口。

构建MongoDB数据类型

import { Schema } from 'mongoose';

var helloSchema = new Schema({
  email:  String,
  lastIP: String,
});

export default global.db.model('Hello', helloSchema);

初始化一个helloSchema,并将这个model命名为hello

还需要一个index.js将数据类型全部引入:

import HelloModel from './HelloModel'

export default {
	HelloModel,
}

构建GraphQL Schema

一个schema需要分成三部分:

  1. mutations
    1. 修改/删除/增加操作
  2. queries
    1. 查询操作
  3. types
    1. 数据类型定义(输入/输出)

以HelloSchema为例,其文件结构如下:

.
├── index.js
├── mutations
│   ├── add.js
│   ├── index.js
│   ├── remove.js
│   └── update.js
├── queries
│   ├── hello.js
│   └── index.js
└── types
    ├── Hello.js
    ├── HelloAddInput.js
    ├── HelloFields.js
    └── HelloUpdateInput.js

各文件作用如下:

  • index.js
    • 初始化query和mutation
  • mutations
    • add.js
      • 执行增加操作
    • index.js
      • 将增加/修改/删除操作引用到一起
    • remove.js
      • 执行删除操作
    • update.js
      • 执行更新操作
  • queries
    • hello.js
      • 执行查询操作
    • index.js
      • 将查询操作引用到一起
  • types
    • Hello.js
      • 定义返回结果的数据类型
    • HelloAddInput.js
      • 定义增加操作时输入结构的数据类型
    • HelloUpdateInput.js
      • 定义更新操作时输入结构的数据类型
    • HelloFields.js
      • 定义HelloSchema的通用数据结构(和HelloModel内容相同)

index.js

初始化query和mutation

import {
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLList
} from 'graphql';

import mutations from './mutations';
import queries from './queries';

let schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: queries
  }),

  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: mutations
  })
});

export default schema;

queries/hello.js

执行查询操作

import { GraphQLList, GraphQLString } from 'graphql';

import HelloType from '../types/Hello.js';
import HelloModel from '../../../models/HelloModel';

const hello = {
	type: new GraphQLList(HelloType),
	async resolve (root, params, options) {
  		var hello = await HelloModel.find({});
  		return hello;
	}
}

export default hello;

定义了返回结果是一个HelloType的数组列表 ,resolve中使用了async函数进行mongodb异步查询。

mutations/add.js

执行增加操作

import { GraphQLNonNull } from 'graphql';

import HelloType from '../types/Hello.js';
import HelloAddInput from '../types/HelloAddInput.js';
import HelloModel from '../../../models/HelloModel';

const add = {
  type: HelloType,
  args: {
    info: {
      name: 'info',
      type: new GraphQLNonNull(HelloAddInput)
    }
  },
  async resolve (root, params, options) {

    const HelloModel = new HelloModel(params.info);
    const newHello = await HelloModel.save();

    if (!newHello) {
      return false;
    }

    return newHello;
  }
};

export default add;

注意其中args,其类型是HelloAddInput

HelloAddInput定义如下:

import {
  GraphQLInputObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLNonNull
} from 'graphql';

export default new GraphQLInputObjectType({
  name: 'HelloAddInput',
  fields: {
    email: {
      type: GraphQLString
    },
    lastIP: {
      type: GraphQLString
    }
  }
});

声明了在执行添加操作时需要输入email和lastIP参数。

mutations/update.js

执行更新操作

import { GraphQLNonNull } from 'graphql';

import HelloType from '../types/Hello.js';
import HelloModel from '../../../models/HelloModel';
import HelloUpdateInput from '../types/HelloUpdateInput.js';

const update = {
  type: HelloType,
  args: {
    options: {
      name: 'options',
      type: new GraphQLNonNull(HelloUpdateInput)
    }
  },
  async resolve (root, params, options) {

    const updated = await HelloModel.findOneAndUpdate({
      _id: params.options._id
    }, params.options);

    const hello = await HelloModel.findOne({
      _id: params.options._id
    });

    return hello;
  }
};

export default update

注意其中的参数options,其数据类型为HelloUpdateInput。

HelloUpdateInput定义如下:

import {
  GraphQLObjectType,
  GraphQLInputObjectType,
  GraphQLNonNull,
  GraphQLString,
  GraphQLID,
  GraphQLInt,
  GraphQLBoolean
} from 'graphql';

import HelloFields from './HelloFields';

export default new GraphQLInputObjectType({
  name: 'HelloUpdateInput',
  fields: HelloFields
});

定义了在更新字段时可以使用HelloFields内的任意字段,HelloFields定义如下:

import {
  GraphQLString,
  GraphQLInt,
  GraphQLBoolean
} from 'graphql';

export default {
  _id: {
    type: GraphQLString
  },
  email: {
    type: GraphQLString
  },
  lastIP: {
    type: GraphQLString
  }
}

其和model是一样的

mutations/remove.js

执行删除操作

import { GraphQLList, GraphQLString } from 'graphql';

import HelloType from '../types/Hello.js';
import HelloModel from '../../../models/HelloModel';

const remove = {
  type: new GraphQLList(HelloType),
  args: {
    ids: {
      name: 'ids',
      type: new GraphQLList(GraphQLString)
    }
  },
  async resolve (root, params, options) {

    let removedList = [];

    for (var i = 0; i < params.ids.length; i++) {
      const _id = params.ids[i];
      const removed = await HelloModel.findOneAndRemove({
        _id
      });

      if(removed) {
        removedList.push(removed)
      }
    };

    return removedList;
  }
}

export default remove

注意其中的ids是一个GraphQLList字符串数组,返回结果是HelloType对象。

执行GraphQL查询

启动程序

docker-compose up -d

打开GraphQL测试界面: http:// localhost:5555/graphiql

先来执行一个查询:

可以看到结果返回空。

再执行一个增加操作:

可以看到右侧返回了新增的数据及其id

我们再将emai修改为“fuck_shit”:

右侧成功返回了修改之后的数据。

最后,我们删除这条数据:

返回了被删除的id。

再最后执行一次查询:

可以看到结果又变为空了。

至此,整个GraphQL+MongoDB的CRUD操作测试成功。

小Tips

graphql的测试器右侧可以查看数据类型:

完整代码见: leinue/node-mongodb-graphql-docker