mongoose实现one-to-many的model关系

发表于 2016-11-01
更新于 2024-05-23
分类于 技术专栏
阅读量 2189
字数统计 9741

前言

最近完成一个简单的express项目,然后就需要用到mongoDb。以前在使用的时候都是基于Sailsjs的,然后它帮你封装好了所有的东西,只需要定义一些model即可,可谓是简单至极。然后现在的项目使用了express,虽然也有mongoose做了封装,但还是需要你做很多事情的,于是就发现了一些自己容易绕进去的坑。。。。不过就喜欢这种挖坑的感觉。

1、初始化mongoose

在app.js文件中添加mongoose包的依赖:

const mongoose = require('mongoose');

之后我们连接mongo服务器:

if ('development' == app.get('env')) {
  mongoose.Promise = global.Promise;
  mongoose.connect(config.mongoUrl);
} else {
  // insert db connection for production
}

config.mongoUrl定义了mongoDB服务器的地址,比如:"mongoUrl": "mongodb://localhost/dwd"

当连接成功的话mongo会自动创建dwd数据库。

接着启动HTTP服务器:

let db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function callback () {
  /**
   * Listen on provided port, on all network interfaces.
   */
  let server = http.createServer(app);
  server.listen(app.get('port'));
  server.on('error', onError);
  server.on('listening', function(){
    var addr = server.address();
    var bind = typeof addr === 'string'
      ? 'pipe ' + addr
      : 'port ' + addr.port;
    debug('Listening on ' + bind);
  });
});

Tips

在Express4.x中,http.createServer()已经不再需要,除非你需要直接操作http。app可以直接使用app.listen()来启动。参考文档:Moving to Express 4

2、定义你的model

假设我们想要实现的数据库是这样的:

一个项目名下有众多的项目文件,每个项目文件下又有众多的版本文件,这种便是典型的一对多映射关系。

于是首先设计Project的Model:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const File = require('./file');

const ProjectSchema = new Schema({
  createdAt : { type: Date, default: Date.now },
  name: {type: String, required: true, index: {unique: true}},
  files: [{type: Schema.Types.ObjectId, ref: 'File'}]
})

module.exports = mongoose.model('Project', ProjectSchema);

上面定义的model有几个关键点:

  1. name为project model的索引并且是唯一的
  2. files的类型是ObjectID,通过它可以索引到File的Model(refs的类型可以是ObjectId, Number, String,和Buffer)。
  3. files是一个数组,也就是说它可以包含很多File Model

根据Mongoose的API文档population中提到的:

在MongoDB中是没有连接的(也就是说非关系型数据库),但是有时我们仍然想要索引到别的集合下的文档,这个时候就是population存在的意义。

接着定义File:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const Version = require('./version');
const Project = require('./project');

const FileSchema = new Schema({
  createdAt : { type: Date, default: Date.now },
  name: {type: String, required: true},
  project_id: { type: Schema.Types.ObjectId, ref: 'Project' },
  versions: [{type: Schema.Types.ObjectId, ref: 'Version'}],
  updateAt: { type: Date, default: Date.now }
})

FileSchema.index({ name: 1, project_id: 1}, { unique: true });

module.exports = mongoose.model('File', FileSchema);

这个model的关键点是定义了nameproject_id,然后将二者做成索引并确保唯一性。同时它的下属version Model也是类似于之前Project中files的定义。

3、定义你的controller

在你的controller实现你的CRUD,一般是会将路由结合到一起:(routes/index.js)

var express = require('express');
var router = express.Router();
var file = require('../controllers/fileController');
var version = require('../controllers/versionController');
var project = require('../controllers/projectController');
var mashup = require('../controllers/mashupController');
var multer  = require('multer');
var storage = multer.memoryStorage();
var upload = multer({ storage: storage });

const configFile = process.env.node_env === 'production' ? './config.prod.json' : './config.json';

/* GET home page. */
router.get('/', project.showAllProjects);
router.post('/addNewFile', upload.array('file', 12), file.addNewFile);
router.post('/addNewVersion',  upload.array('file', 12), version.addNewVersion);
router.post('/addNewProject', upload.array('file', 12), project.addNewProject);

module.exports = router;

3.1、findOne vs find

在controller中实现新增project、file、version的功能。其中实现过程中就肯定会使用到find或findOne的方法,在国外开发者的一篇文章中是不推荐使用findOne方法:Checking if a document exists – MongoDB slow findOne vs find

原因是:findOne方法总是读取并返回整个文档如果存在的话。但是find()只是返回一个指针(或者啥都不返回)并且当你通过指针遍历的时候也只是读取其数据,所以性能会比findOne()高很多,这篇文章的作者还给出了测试的结果。

另外值得注意的一点是:

使用find或findOne方法的时候,如果没有找到对应的record那么只会返回null或[],而不会报错,所以你不应该去捕捉这两个函数的错误去判断是否存在该record而是应该去判断返回的结果是否是null或数组的长度为0!!

衍生的Mongoose API:

  1. Model.find(conditions, [projection], [options], [callback])
  2. Model.findById(id, [projection], [options], [callback])
  3. Model.findByIdAndRemove(id, [options], [callback])
  4. Model.findByIdAndUpdate(id, [update], [options], [callback])
  5. Model.findOne([conditions], [projection], [options], [callback])
  6. Model.findOneAndRemove(conditions, [options], [callback])
  7. Model.findOneAndUpdate([conditions], [update], [options], [callback])

3.2、如何实现project和file或file和version的映射关系?

根据mongoose的population文档,one-to-many中,one一方需要保存many一方对应的ObjectId,所以其文档的Refs to children小节中提供了其方法:

aaron.stories.push(story1);
aaron.save(callback);

3.3、映射关系建立完成之后

如果新建完所有的信息之后,我们可以使用populate来获取对应关联的集合的文档,比如获取所有的projects下所有的file和versions:

  ProjectModel.find({}).populate({
    path: 'files',
    populate: {path: 'versions'}
  }).exec(function(err, projects){
    return res.render('index', {
      projects: projects
    })
  })

因为populate支持多层嵌套populate,所以可以像刚才的那样写法,populate出所有的相关files和version,如果你想populate出来的document指定的字段,可以使用select关键词,如果想匹配某个条件,可以使用match关键词,如果想限制个数可以使用option中的limit关键词,比如:

  ProjectModel.find({}).populate({
    path: 'files',
    populate: {
        path: 'versions'
        select: 'version _id',
        options: { limit: 5 }
        match: { version: 'v0.1'},
    }
  }).exec(function(err, projects){
    return res.render('index', {
      projects: projects
    })
  })

match中也可以使用诸如$gt等在Operators中罗列的操作符。

3.4、执行Update

Mongoose提供Model.update(conditions, doc, [options], [callback])Query#findOneAndUpdate([query], [doc], [options], [callback])以及Query#update([criteria], [doc], [options], [callback])来执行更新操作。根据其前缀可以看出区别来:前者是Model下的方法,后两个是Query下的方法。

3.4.1、Model.update

在数据库更新文档并且不需要返回被更新的文档,只是返回更新的状态。该方法提供的options:

  1. safe(boolean)安全模式(默认该值设为true)
  2. upsert(boolean)是否在没匹配到文档的情况新建一个(false)
  3. multi(boolean)是否应该更新多个文档
  4. runValidators如果为true,在这个命令上执行update validators。更新检验器会校验更新操作。
  5. setDefaultsOnInsert如果这个和upsert都置为true,Mongoose将会应用model中的默认值当新建文档时。该选项只在MongoDb>=2.4的时候才成立因为它依赖于MongoDB的$setOnInsert操作符
  6. strict(boolean)为这次更新重写strict选项
  7. overwrite(boolean)关闭只更新模式,允许你重写文档(false)

在Mongoose中当你执行:

var query = { name: 'borne' };
Model.update(query, { name: 'jason borne' }, options, callback)

的时候,Mongoose将会自动帮你加上$set关键词,除非你开启了overwrite选项:

Model.update(query, { $set: { name: 'jason borne' }}, options, callback)

这样可以帮你防止意外地重写了整个文档。

如果不需要等待Mongo的回应的话就不要传递callback,而是直接在它返回的Query下执行exec()

在该方法下,下面的特性都无法使用:

  1. defaults
  2. setters
  3. validators
  4. middleware

如果需要使用这些,那么还是得用传统的老办法:findOne && save

3.4.2、Query#findOneAndUpdate

该方法是直接调用 findAndModify更新命令。提供的options如下:

  1. new: bool - 如果为true,那么返回更新过的文档而不是原始文档。默认为false也就是返回原始没更改过的文档
  2. upsert(boolean)是否在没匹配到文档的情况新建一个(false)
  3. fields: {Object|String} - 字段选择器等价于.select(fields).findOneAndUpdate()
  4. sort: 如果匹配到多个文档,设置排列顺序以选择需要更新的文档
  5. maxTimeMS: 在查询上设置一个时间限制,要求mongoDB >= 2.6.0
  6. runValidators如果为true,在这个命令上执行update validators。更新检验器会校验更新操作。
  7. setDefaultsOnInsert如果这个和upsert都置为true,Mongoose将会应用model中的默认值当新建文档时。该选项只在MongoDb>=2.4的时候才成立因为它依赖于MongoDB的$setOnInsert操作符
  8. passRawResult: 如果为true,从MongoDB驱动中传递原始的结果并作为回调的第三个参数
  9. context: (string)如果设置为query并且runValidators打开,该值可以在更新校验运行时在自定义的校验器中引用到查询结果,如果runValidators为false什么事也不做。

3.4.3、Query#update

申明并执行该查询作为更新的操作。提供的options:

  1. safe(boolean)安全模式(默认该值设为true)
  2. upsert(boolean)是否在没匹配到文档的情况新建一个(false)
  3. multi(boolean)是否应该更新多个文档
  4. runValidators如果为true,在这个命令上执行update validators。更新检验器会校验更新操作。
  5. setDefaultsOnInsert如果这个和upsert都置为true,Mongoose将会应用model中的默认值当新建文档时。该选项只在MongoDb>=2.4的时候才成立因为它依赖于MongoDB的$setOnInsert操作符
  6. strict(boolean)为这次更新重写strict选项
  7. overwrite(boolean)关闭只更新模式,允许你重写文档(false)
  8. context: (string)如果设置为query并且runValidators打开,该值可以在更新校验运行时在自定义的校验器中引用到查询结果,如果runValidators为false什么事也不做。

具体操作范例参考Mongoose API

参考:

  1. https://docs.mongodb.com/manual/reference/method/db.collection.find/
  2. https://docs.mongodb.com/v3.2/reference/method/db.collection.findOne/
  3. http://mongoosejs.com/docs/populate.html
  4. https://docs.mongodb.com/v3.2/reference/operator/aggregation/

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 mongoose实现one-to-many的model关系 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github