Node.js 中操作 MongoDB拟蜻,相信首選的庫(kù)肯定是 mongoose 了,但是實(shí)際項(xiàng)目中如果想要使用枯饿,還需要自己做封裝酝锅。但是考慮到 mongoose 長(zhǎng)長(zhǎng)的文檔,讓好多人望而怯步奢方。 筆者當(dāng)時(shí)打算用 mongoose 的時(shí)候搔扁,網(wǎng)上轉(zhuǎn)了一圈,發(fā)現(xiàn)多數(shù)就是抄抄官方文檔蟋字,于是硬著頭皮讀了一遍官方文檔稿蹲,依據(jù)項(xiàng)目需求做了簡(jiǎn)要封裝。
mongoose 非常的漂亮鹊奖,所以封裝的過(guò)程也非常的簡(jiǎn)潔苛聘。項(xiàng)目傳送門(mén)
筆者在擼碼的時(shí)候,是將結(jié)構(gòu)分成以下幾部分:
- 連接層
- Model 層
- Dao 層
- 應(yīng)用層
1. 連接層
代碼先放下邊了忠聚。代碼里注釋都已經(jīng)很明白了设哗,這里有幾點(diǎn)需要注意下:
- 要是用 Node.js 自帶的 Promise 替換 mongoose 中的 Promise,否則有時(shí)候會(huì)報(bào)警告
- 關(guān)于 MongoDB 的所有配置两蟀,我引用了 config 庫(kù)熬拒,配置都放在了 default.json 中
- 在使用
mongoose.createConnection()
,并不是簡(jiǎn)單創(chuàng)建了一個(gè) mongo client垫竞,而是其內(nèi)部創(chuàng)建了一個(gè)連接池,默認(rèn)是維護(hù)了 5 個(gè) client蛀序。 而這個(gè)連接池我們可以根據(jù)自己的硬件和需求情況自定義欢瞪,只需要設(shè)置options
中的poolSize
參數(shù)即可。 - 如果你是單機(jī)的 mongoDB徐裸,那么只需要配置 default.json 文件中的
host
和port
即可遣鼓,replicaSet 中的name
字段設(shè)置成空串就好 - 如果你要使用 mongoDB 集群配置,那就要用到
replicaSet
重贺,此時(shí)填寫(xiě) default.json 中相應(yīng)的replicaSet
配置即可骑祟。我在讀取配置文件再做相應(yīng)連接的時(shí)候邏輯都已經(jīng)處理了回懦。
// 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 的話是全局配置次企,推薦在 每個(gè) Model 內(nèi) 自己定義
// mongoose.plugin(require('./plugin').updatedAt);
/**
* 配置 MongoDb options
*/
function getMongoOptions() {
let options = {
useMongoClient: true,
poolSize: 5, // 連接池中維護(hù)的連接數(shù)
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');
return options;
}
/**
* 拼接 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); // 去掉末尾逗號(hào)
} else {
mongoUri += `${mongodbConfig.get('host')}:${mongodbConfig.get('port')}`;
}
mongoUri += `/${dbName}`;
return mongoUri;
}
/**
* 創(chuàng)建 Mongo 連接怯晕,內(nèi)部維護(hù)了一個(gè)連接池,全局共享
*/
let mongoClient = mongoose.createConnection(getMongoUri(), getMongoOptions());
/**
* Mongo 連接成功回調(diào)
*/
mongoClient.on('connected', function () {
console.log('Mongoose connected to ' + getMongoUri());
});
/**
* Mongo 連接失敗回調(diào)
*/
mongoClient.on('error', function (err) {
console.log('Mongoose connection error: ' + err);
});
/**
* Mongo 關(guān)閉連接回調(diào)
*/
mongoClient.on('disconnected', function () {
console.log('Mongoose disconnected');
});
/**
* 關(guān)閉 Mongo 連接
*/
function close() {
mongoClient.close();
}
module.exports = {
mongoClient: mongoClient,
close: close,
};
2. Model
這一層我以創(chuàng)建 book collection 舉例缸棵。要?jiǎng)?chuàng)建一個(gè) Model舟茶,我將它分成以下幾部分
schema
plugin(后面單獨(dú)再說(shuō)這個(gè))
hook
-
最后通過(guò)
mongoClient.model()
創(chuàng)建出真正的 Model// book.js let {Schema} = require('mongoose'); let {mongoClient} = require('../lib/mongo'); /** * 操作 MongoDb 時(shí)需要?jiǎng)?chuàng)建兩個(gè)文件 model.js 和 modelDao.js * * 一. 對(duì)于 Model.js 以下幾部分: * 1. Schema 必要 * 2. plugin 可選 * 3. hook 可選 * 4. 調(diào)用 mongoClient.model() 創(chuàng)建 Model,此處注意堵第,Model 名稱與 js 文件名一樣吧凉,但首字母大寫(xiě) * * 二. 對(duì)于 modelDao.js * 我們需要聲明一個(gè) ModelDao 的 class 繼承自 BaseDao, BaseDao 中包含基本的 crud 操作踏志,也可以根據(jù)需求自行定義 * * 三. 外部使用 * var dao = new ModelDao() * dao.crud(); */ const bookSchema = new Schema( { title: String, author: {type: String, lowercase: true}, // lowercase 都屬于 setters }, { runSettersOnQuery: true // 查詢時(shí)是否執(zhí)行 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(); // 如果有下一個(gè) pre(), 則執(zhí)行下一個(gè) pre(), 否則 執(zhí)行 save() // }); // bookSchema.post('save', function (result, next) { // console.log('---------post1----------', result); // next(); // }); // // bookSchema.post('save', function (result, next) { // console.log('---------post2----------', result); // next(); // 如果有下一個(gè) post(), 則執(zhí)行下一個(gè) post(), 否則 結(jié)束 // }); // })(); /** * 參數(shù)一要求與 Model 名稱一致 * 參數(shù)二為 Schema * 參數(shù)三為映射到 MongoDB 的 Collection 名 */ let Book = mongoClient.model(`Book`, bookSchema, 'book'); module.exports = Book;
3. Dao 層
這部分代碼是 Dao 層的基類阀捅,封裝了基本的 crud 操作,在實(shí)現(xiàn)它的時(shí)候傳入對(duì)應(yīng)的 Model 即可针余∷潜桑可以參照項(xiàng)目中的 bookDao.js,如果不同的 dao 中需要定制不同的數(shù)據(jù)庫(kù)操作,那么只需要在實(shí)現(xiàn)類中增加相應(yīng)的方法即可涵紊。
// baseDao.js
class BaseDao {
/**
* 子類構(gòu)造傳入對(duì)應(yīng)的 Model 類
*
* @param Model
*/
constructor(Model) {
this.Model = Model;
}
/**
* 使用 Model 的 靜態(tài)方法 create() 添加 doc
*
* @param obj 構(gòu)造實(shí)體的對(duì)象
* @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 構(gòu)造實(shí)體的對(duì)象
* @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. 外部調(diào)用
new 對(duì)應(yīng)的 dao 實(shí)例傍妒,然后調(diào)用對(duì)應(yīng)的數(shù)據(jù)庫(kù)操作就好了
let bookDao = new BookDao();
bookDao.crud();
5. plugin
在不同的 collection 中可能會(huì)有一些通用的操作,這時(shí)候摸柄,我們就可以把這種操作封裝進(jìn) plugin 中颤练。 比如 doc 的 插入時(shí)間和更改時(shí)間。下面是一個(gè)例子:
/**
* doc 創(chuàng)建時(shí)間 plugin驱负。其中在 插入一條 doc 時(shí) 同時(shí)創(chuàng)建 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 更新時(shí)間 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,
};