Node.js 中操作 MongoDB,相信首选的库肯定是 mongoose 了,但是实际项目中如果想要使用,还需要自己做封装。但是考虑到 mongoose 长长的文档,让好多人望而怯步。 笔者当时打算用 mongoose 的时候,网上转了一圈,发现多数就是抄抄官方文档,于是硬着头皮读了一遍官方文档,依据项目需求做了简要封装。
mongoose 非常的漂亮,所以封装的过程也非常的简洁。项目传送门
笔者在撸码的时候,是将结构分成以下几部分:
- 连接层
- Model 层
- Dao 层
- 应用层
1. 连接层
代码先放下边了。代码里注释都已经很明白了,这里有几点需要注意下:
- 要是用 Node.js 自带的 Promise 替换 mongoose 中的 Promise,否则有时候会报警告
- 关于 MongoDB 的所有配置,我引用了 config 库,配置都放在了 default.json 中
- 在使用
mongoose.createConnection()
,并不是简单创建了一个 mongo client,而是其内部创建了一个连接池,默认是维护了 5 个 client。 而这个连接池我们可以根据自己的硬件和需求情况自定义,只需要设置options
中的poolSize
参数即可。 - 如果你是单机的 mongoDB,那么只需要配置 default.json 文件中的
host
和port
即可,replicaSet 中的name
字段设置成空串就好 - 如果你要使用 mongoDB 集群配置,那就要用到
replicaSet
,此时填写 default.json 中相应的replicaSet
配置即可。我在读取配置文件再做相应连接的时候逻辑都已经处理了。
// mongo.js let mongoose = require('mongoose'); let mongodbConfig = require('config').get('mongodb'); /** * debug 模式 */ // mongoose.set('debug', true); /** * 使用 Node 自带 Promise 代替 mongoose 的 Promise */ mongoose.Promise = global.Promise; // 配置 plugin。此处配置 plugin 的话是全局配置,推荐在 每个 Model 内 自己定义 // mongoose.plugin(require('./plugin').updatedAt); /** * 配置 MongoDb options */ function getMongoOptions() { let options = { useMongoClient: true, poolSize: 5, // 连接池中维护的连接数 reconnectTries: Number.MAX_VALUE, keepAlive: 120, }; if (mongodbConfig.get('user')) options.user = mongodbConfig.get('user'); if (mongodbConfig.get('pass')) options.pass = mongodbConfig.get('pass'); if (mongodbConfig.get('replicaSet').get('name')) options.replicaSet = mongodbConfig.get('replicaSet').get('name'); } /** * 拼接 MongoDb Uri * * @returns {string} */ function getMongoUri() { let mongoUri = 'mongodb://'; let dbName = mongodbConfig.get('db'); let replicaSet = mongodbConfig.get('replicaSet'); if (replicaSet.get('name')) { // 如果配置了 replicaSet 的名字 则使用 replicaSet let members = replicaSet.get('members'); for (let member of members) { mongoUri += `${member.host}:${member.port},`; } mongoUri = mongoUri.slice(0, -1); // 去掉末尾逗号 } else { mongoUri += `${mongodbConfig.get('host')}:${mongodbConfig.get('port')}`; } mongoUri += `/${dbName}`; return mongoUri; } /** * 创建 Mongo 连接,内部维护了一个连接池,全局共享 */ let mongoClient = mongoose.createConnection(getMongoUri(), getMongoOptions()); /** * Mongo 连接成功回调 */ mongoClient.on('connected', function () { console.log('Mongoose connected to ' + getMongoUri()); }); /** * Mongo 连接失败回调 */ mongoClient.on('error', function (err) { console.log('Mongoose connection error: ' + err); }); /** * Mongo 关闭连接回调 */ mongoClient.on('disconnected', function () { console.log('Mongoose disconnected'); }); /** * 关闭 Mongo 连接 */ function close() { mongoClient.close(); } module.exports = { mongoClient: mongoClient, close: close, };
2. Model
这一层我以创建 book collection 举例。要创建一个 Model,我将它分成以下几部分
- schema
- plugin(后面单独再说这个)
- hook
- 最后通过
mongoClient.model()
创建出真正的 Model
// book.js let {Schema} = require('mongoose'); let {mongoClient} = require('../lib/mongo'); /** * 操作 MongoDb 时需要创建两个文件 model.js 和 modelDao.js * * 一. 对于 Model.js 以下几部分: * 1. Schema 必要 * 2. plugin 可选 * 3. hook 可选 * 4. 调用 mongoClient.model() 创建 Model,此处注意,Model 名称与 js 文件名一样,但首字母大写 * * 二. 对于 modelDao.js * 我们需要声明一个 ModelDao 的 class 继承自 BaseDao, BaseDao 中包含基本的 crud 操作,也可以根据需求自行定义 * * 三. 外部使用 * var dao = new ModelDao() * dao.crud(); */ const bookSchema = new Schema( { title: String, author: {type: String, lowercase: true}, // lowercase 都属于 setters }, { runSettersOnQuery: true // 查询时是否执行 setters } ); /** * 配置 plugin */ // (function () { // let plugin = require('./plugin'); // bookSchema.plugin(plugin.createdAt); // bookSchema.plugin(plugin.updatedAt); // })(); /** * 配置 hook */ // (function () { // bookSchema.pre('update', function (next) { // console.log('pre update'); // next(); // }); // // bookSchema.post('update', function (result, next) { // console.log('post update', result); // next(); // }); // // bookSchema.pre('save', function (next) { // console.log('--------pre1------'); // next(); // }); // // bookSchema.pre('save', function (next) { // console.log('--------pre2------'); // next(); // 如果有下一个 pre(), 则执行下一个 pre(), 否则 执行 save() // }); // bookSchema.post('save', function (result, next) { // console.log('---------post1----------', result); // next(); // }); // // bookSchema.post('save', function (result, next) { // console.log('---------post2----------', result); // next(); // 如果有下一个 post(), 则执行下一个 post(), 否则 结束 // }); // })(); /** * 参数一要求与 Model 名称一致 * 参数二为 Schema * 参数三为映射到 MongoDB 的 Collection 名 */ let Book = mongoClient.model(`Book`, bookSchema, 'book'); module.exports = Book;
3. Dao 层
这部分代码是 Dao 层的基类,封装了基本的 crud 操作,在实现它的时候传入对应的 Model 即可。可以参照项目中的 bookDao.js,如果不同的 dao 中需要定制不同的数据库操作,那么只需要在实现类中增加相应的方法即可。
// baseDao.js class BaseDao { /** * 子类构造传入对应的 Model 类 * * @param Model */ constructor(Model) { this.Model = Model; } /** * 使用 Model 的 静态方法 create() 添加 doc * * @param obj 构造实体的对象 * @returns {Promise} */ create(obj) { return new Promise((resolve, reject) => { let entity = new this.Model(obj); this.Model.create(entity, (error, result) => { if (error) { console.log('create error--> ', error); reject(error); } else { console.log('create result--> ', result); resolve(result) } }); }); } /** * 使用 Model save() 添加 doc * * @param obj 构造实体的对象 * @returns {Promise} */ save(obj) { return new Promise((resolve, reject) => { let entity = new this.Model(obj); entity.save((error, result) => { if (error) { console.log('save error--> ', error); reject(error); } else { console.log('save result--> ', result); resolve(result) } }); }); } /** * 查询所有符合条件 docs * * @param condition 查找条件 * @param constraints * @returns {Promise} */ findAll(condition, constraints) { return new Promise((resolve, reject) => { this.Model.find(condition, constraints ? constraints : null, (error, results) => { if (error) { console.log('findAll error--> ', error); reject(error); } else { console.log('findAll results--> ', results); resolve(results); } }); }); } /** * 查找符合条件的第一条 doc * * @param condition * @param constraints * @returns {Promise} */ findOne(condition, constraints) { return new Promise((resolve, reject) => { this.Model.findOne(condition, constraints ? constraints : null, (error, results) => { if (error) { console.log('findOne error--> ', error); reject(error); } else { console.log('findOne results--> ', results); resolve(results); } }); }); } /** * 查找排序之后的第一条 * * @param condition * @param orderColumn * @param orderType * @returns {Promise} */ findOneByOrder(condition, orderColumn, orderType) { return new Promise((resolve, reject) => { this.Model.findOne(condition) .sort({[orderColumn]: orderType}) .exec(function (err, record) { console.log(record); if (err) { reject(err); } else { resolve(record); } }); }); } /** * 更新 docs * * @param condition 查找条件 * @param updater 更新操作 * @returns {Promise} */ update(condition, updater) { return new Promise((resolve, reject) => { this.Model.update(condition, updater, (error, results) => { if (error) { console.log('update error--> ', error); reject(error); } else { console.log('update results--> ', results); resolve(results); } }); }); } /** * 移除 doc * * @param condition 查找条件 * @returns {Promise} */ remove(condition) { return new Promise((resolve, reject) => { this.Model.remove(condition, (error, result) => { if (error) { console.log('remove error--> ', error); reject(error); } else { console.log('remove result--> ', result); resolve(result); } }); }); } } module.exports = BaseDao;
4. 外部调用
new 对应的 dao 实例,然后调用对应的数据库操作就好了
let bookDao = new BookDao(); bookDao.crud();
5. plugin
在不同的 collection 中可能会有一些通用的操作,这时候,我们就可以把这种操作封装进 plugin 中。 比如 doc 的 插入时间和更改时间。下面是一个例子:
/** * doc 创建时间 plugin。其中在 插入一条 doc 时 同时创建 createdAt 和 updateAt 字段 * * @param schema * @param options */ function createdAt(schema, options) { schema.add({createdAt: Date}); schema.add({updatedAt: Date}); schema.pre('save', function (next) { let now = Date.now(); this.createdAt = now; this.updatedAt = now; next(); }); if (options && options.index) { schema.path('createdAt').index(options.index); schema.path('updatedAt').index(options.index); } } /** * doc 更新时间 plugin * * @param schema * @param options */ function updatedAt(schema, options) { schema.pre('update', function (next) { this.update({}, {$set: {updatedAt: new Date()}}); next(); }); if (options && options.index) { schema.path('updatedAt').index(options.index); } } module.exports = { createdAt: createdAt, updatedAt: updatedAt, };