Skip to content

05 如何操作MongoDB

孙正华 edited this page Jul 26, 2018 · 5 revisions

Node.js 操作 MongoDB,可以使用 MongoDB 自家为 Node.js 开发的驱动 node-mongodb-native,也可以使用 Mongoose,后者是一个 ORM 框架,更能体现面向对象的思想,但性能逊于前者。
iBlog2 使用 Mongoose 来操作 MongoDB,并搭配模块 mongoose-schema-extend 创建每一个数据模型的基类。

安装 mongoose & mongoose-schema-extend

$ npm install --save mongoose
$ npm install --save mongoose-schema-extend

连接 MongoDB

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