-
Notifications
You must be signed in to change notification settings - Fork 396
05 如何操作MongoDB
Node.js 操作 MongoDB,可以使用 MongoDB 自家为 Node.js 开发的驱动 node-mongodb-native,也可以使用 Mongoose,后者是一个 ORM 框架,更能体现面向对象的思想,但性能逊于前者。
iBlog2 使用 Mongoose 来操作 MongoDB,并搭配模块 mongoose-schema-extend 创建每一个数据模型的基类。
$ npm install --save mongoose
$ npm install --save mongoose-schema-extend
https://github.com/eshengsky/iBlog2/blob/master/models/db.js#L1-L14
const dbPath = require('../config')
.mongoUrl;
const mongoose = require('mongoose');
const extend = require('mongoose-schema-extend');
const i18n = require('./i18n');
// use custom mongodb url or localhost
mongoose.connect(dbPath || 'mongodb://localhost/blogrift');
const db = mongoose.connection;
db.on('error', err => {
console.error(i18n.__('error.db_1') + err);
process.exit(1);
});
exports.mongoose = mongoose;
创建基类型,基类型可以定义所有文档都包含的一些字段,如id、创建时间、修改时间、操作人等等。
https://github.com/eshengsky/iBlog2/blob/master/models/db.js#L16-L27
// 基础Schema
const base = new mongoose.Schema({
// 唯一键
_id: { type: String, unique: true },
// 创建时间
CreateTime: { type: Date },
// 修改时间
ModifyTime: { type: Date }
});
exports.base = base;
创建一个数据模型,会继承基类型中的属性。
https://github.com/eshengsky/iBlog2/blob/master/models/post.js#L5-L43
const postSchema = base.extend({
// 标题
Title: { type: String },
// 文章别名
Alias: { type: String },
// 摘要
Summary: { type: String },
// 来源
Source: { type: String },
// 内容
Content: { type: String },
// 内容类型:默认空 (html),可选markdown
ContentType: { type: String },
// 分类Id
CategoryId: { type: String },
// 标签
Labels: { type: String },
// 外链Url
Url: { type: String },
// 浏览次数
ViewCount: { type: Number },
// 是否草稿
IsDraft: { type: Boolean },
// 是否有效
IsActive: { type: Boolean, default: true }
});
exports.PostModel = mongoose.model('post', postSchema, 'post');
注意: 使用 mongoose.model()
时,建议总是传入第三个参数来作为集合(collection)的名称,否则 mongoose 会自动采用英文的复数格式作为集合名称,容易让人困惑。
- 使用 findById 根据文档的唯一键查询该文档。
https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L282-L287
postModel.findById(id, (err, article) => {
if (err) {
return callback(err);
}
return callback(null, article);
});
- 使用 findOne 根据一个条件对象查询该文档。
https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L262-L273
postModel.findOne({ Alias: alias }, (err, article) => {
if (err) {
return callback(err);
}
if (!article) {
return callback(null, true);
}
if (article._id === articleId) {
return callback(null, true);
}
return callback(null, false);
});
- 使用 find 根据一个条件对象查询符合的所有数据。
https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L11-L54 https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L70-L90
/**
* 为首页数据查询构建条件对象
* @param params 查询参数对象
* @returns {{}}
*/
function getPostsQuery(params) {
const query = {};
query.IsActive = true;
query.IsDraft = false;
if (params.cateId) {
query.CategoryId = params.cateId;
}
if (params.keyword) {
switch (params.filterType) {
case '1':
query.Title = { $regex: params.keyword, $options: 'gi' };
break;
case '2':
query.Labels = { $regex: params.keyword, $options: 'gi' };
break;
case '3':
query.CreateTime = { $regex: params.keyword, $options: 'gi' };
break;
default:
query.$or = [{
Title: {
$regex: params.keyword,
$options: 'gi'
}
}, {
Labels: {
$regex: params.keyword,
$options: 'gi'
}
}, {
Summary: {
$regex: params.keyword,
$options: 'gi'
}
}, {
Content: {
$regex: params.keyword,
$options: 'gi'
}
}];
}
}
return query;
}
let page = parseInt(params.pageIndex) || 1;
const size = parseInt(params.pageSize) || 10;
page = page > 0 ? page : 1;
const options = {};
options.skip = (page - 1) * size;
options.limit = size;
options.sort = params.sortBy === 'title' ? 'Title -CreateTime' : '-CreateTime';
const query = getPostsQuery(params);
postModel.find(query, {}, options, (err, posts) => {
if (err) {
return callback(err);
}
if (posts) {
redisClient.setItem(cache_key, posts, redisClient.defaultExpired, err => {
if (err) {
return callback(err);
}
});
}
return callback(null, posts);
});
第三个参数 [options]
可以用来设置分页、排序等。
生成一个新的实体对象:
https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L297-L310
const entity = new postModel({
Title: params.Title,
Alias: params.Alias,
Summary: params.Summary,
Source: params.Source,
Content: params.Content,
ContentType: params.ContentType || '',
CategoryId: params.CategoryId,
Labels: params.Labels,
Url: params.Url,
IsDraft: params.IsDraft === 'True',
IsActive: params.IsActive === 'True',
ModifyTime: new Date()
});
调用对象方法 save 插入该文档:
https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L321-L326
entity.save(err => {
if (err) {
return callback(err);
}
return callback(null);
});
如果想批量插入文档,可以调用原生 insert
方法,传入的第一个参数是 json 数组:
https://github.com/eshengsky/iBlog2/blob/master/proxy/category.js#L182-L190
// 插入全部分类
// categoryModel.create(jsonArray, function (err) {}); //不用这个,因为这个内部实现依然是循环插入,不是真正的批量插入
// 这里采用mongodb原生的insert来批量插入多个文档
categoryModel.collection.insert(jsonArray, err => {
if (err) {
return callback(err);
}
return callback(null);
});
使用 update 或者 findOneAndUpdate 更新文档。
https://github.com/eshengsky/iBlog2/blob/master/proxy/post.js#L131-L132
postModel.update({ Alias: alias }, { $inc: { ViewCount: 1 } })
.exec();
https://github.com/eshengsky/iBlog2/blob/master/proxy/category.js#L169-L173
post.update({ $or: updateQuery }, { CategoryId: 'other' }, { multi: true }, err => {
if (err) {
return callback(err);
}
});
使用 remove 或者 findOneAndRemove 删除文档。
https://github.com/eshengsky/iBlog2/blob/master/proxy/category.js#L177
// 将分类全部删除
categoryModel.remove(err => {...});
MongoDB 自身并不提供事务,如果你需要使用事务的功能,可以自己监听错误并及时回滚,或者直接使用 mongoose-transaction。