今天是端午节,首先祝大家端午节快乐,然后今天要和大家说一下最近对 think-mongo 模块做了一些升级。ThinkJS 3 虽然已经支持使用 think-mongoose 来接入 mongoose 模块,不过因为文档这块默认还是 think-mongo 所以用这个模块的同学还是比较多。而这个模块因为是两三年前开发的了,依赖的 mongodb 模块一直是 2.x 的版本,适配 MongoDB 数据库 2.0 版本。而 MongoDB 在 2015 年的时候就升级到了 3.0 版本,2018 年的时候升级到了 4.0 的版本。其中 4.0 的版本增加了事务的功能,这个让许多同学非常心动。所以很多同学提 issue 问能否将 think-mongo 升级一下支持最新版的 MongoDB。

想要适配 MongoDB 其实就是要升级底层依赖的 mongodb 模块,而它在适配新版的同时修改了大量的 API 接口,导致我们没办法直接修改版本号搞定这件事情。之前因为业务中没有 MongoDB 数据库的需求,所以我们也就没有对这个事情做处理,原本是希望有需求的同学可以自行提 PullRequest 的。不过恰好最近新项目中有需要使用 MongoDB 的事务,所以我这边就对其进行了新版本适配的处理。

API 适配

大部分的 API 变更可以在 CHANGES_3.0.0.md 这个文档中找到。首先是之前所有在 Db 类上的方法拆分成了 Client 类和 Db 类,这个影响了 MongoDB 创建连接时的逻辑。

//之前的写法
MongoClient.connect('mongodb://localhost:27017/test', (err, db) => {
  // Database returned
});

//现在的写法
MongoClient.connect('mongodb://localhost:27017/test', (err, client) => {
  // Client returned
  var db = client.db('test');
});

另外就是增删改查的 API 做了修改,包括但不局限于

  • collection.insert() 被细化成了 collection.insertOne()collection.insertMany()
  • collection.remove() 被细化成了 collection.deleteOne()collection.deleteMany()
  • collection.update() 被细化成了 collection.updateOne()collection.updateMany()
  • collection.find(where, field) 被拆分成了 collection.find(where).project(field)

可以看到操作都分成了单个操作和多个操作。think-mongo 中因为有 add()addMany() 所以正好可以对应 insertOne()insertMany() ,而 update()delete() 是没有做单个和多个的区别的,所以只能映射到 updateMany()deleteMany() 方法。

还有一个修改是 collection.aggregate() 方法,原先直接返回结果现在返回后还需要执行下 cursor.toArray() 才能拿到结果。

事务

升级后就可以使用 MongDB 原生的事务特性了。MongoDB 的事务操作和 SQL 的事务操作不太一样,SQL 类的是通过 START TRANSACTIONCOMMITROLLBACK 语句标记来记录一次事务的。MongoDB 的我觉得更像是 JS 操作。

const client = new MongoClient(uri);
await client.connect();
const session = client.startSession();

try {
  session.startTransaction();
  await client.db('think_db').add({name: 'thinkjs'}, {session});
  await session.commitTransaction();
} catch(e) {
  await session.abortTransaction();
} finally {
  await session.endSession();
}

await client.close();

可以看到 MongoDB 会给每次事务创建一个 session 会话记录,所有的 CURD 操作需要将会话标记通过参数的形式传入进去,这样所有的操作就能挂载到当前的事务会话中。最后通过 commitTransactionabortTransaction() 来对事务提交进行操作。基于这个原理我们包装了 transaction() 方法,用来帮助大家方便的实现事务操作。

// src/controller/user.js
module.exports = class extends think.Controller{
  async indexAction() {
    const UserModel = this.mongo('user');
    const PostModel = this.mongo('post');
    await UserModel.transaction(async session => {
      PostModel.options.session = session;
      const userId = await UserModel.add({name: 'lizheming'});
      await PoserModel.add({userId, content: 'Hello World'});
    });
  }
}

可以看到我们并没有在 add() 操作中将 session 会话标记传入,这是因为我们会将它放在示例的 options 属性中,之后的所有的 CURD 操作都会将它透传下去,这样就避免了我们人工显式地去传递它了。同时由于 session 标记只在当前创建事务的表中存在,所以在进行多表操作的时候,为了让其它表也能透传会话标记,需要显式的进行标记赋值操作PostModel.options.session = session;,保证跨表操作都能记录在一次事务中。

后记

由于事务需要 MongoDB 开启集群模式,而我们在 Travis CI 上跑单元测试的时候依赖了它提供的 mongodb 服务,为了让这个服务能切换成集群模式顺利将单元测试跑成功我又做了好多无意义的提交。主要是网上查到的资料也都比较老了,很多 MongoDB 的配置都做了修改,比如找到的资料是 toml 格式的配置,而新的配置已经是 yaml 的配置了。当然这些也都是我踩了好几下坑才发现的就是了。

以上就是 think-mongo 升级适配最新的 MongoDB 4.x 的一些总结,内部的适配都已经在 [email protected] 版本中集成,有需要的同学可以升级下对应的模块使用。如果项目中有直接使用 mongodb 模块提供的原生方法操作数据的话需要注意按照本文说的一些变更进行修改适配,切记!

最后的最后,再一次祝大家端午节快乐。2020 年注定是不平凡的一年,大家且行且珍惜。

03-05 22:50