mongoose实现one-to-many的model关系
前言
最近完成一个简单的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有几个关键点:
- name为project model的索引并且是唯一的
- files的类型是ObjectID,通过它可以索引到File的Model(refs的类型可以是ObjectId, Number, String,和Buffer)。
- 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的关键点是定义了name
和project_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:
- Model.find(conditions, [projection], [options], [callback])
- Model.findById(id, [projection], [options], [callback])
- Model.findByIdAndRemove(id, [options], [callback])
- Model.findByIdAndUpdate(id, [update], [options], [callback])
- Model.findOne([conditions], [projection], [options], [callback])
- Model.findOneAndRemove(conditions, [options], [callback])
- 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:
safe
(boolean)安全模式(默认该值设为true)upsert
(boolean)是否在没匹配到文档的情况新建一个(false)multi
(boolean)是否应该更新多个文档runValidators
如果为true,在这个命令上执行update validators。更新检验器会校验更新操作。setDefaultsOnInsert
如果这个和upsert
都置为true,Mongoose将会应用model中的默认值当新建文档时。该选项只在MongoDb>=2.4的时候才成立因为它依赖于MongoDB的$setOnInsert
操作符strict
(boolean)为这次更新重写strict
选项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()
。
在该方法下,下面的特性都无法使用:
- defaults
- setters
- validators
- middleware
如果需要使用这些,那么还是得用传统的老办法:findOne
&& save
。
3.4.2、Query#findOneAndUpdate
该方法是直接调用 findAndModify更新命令。提供的options如下:
new
: bool - 如果为true,那么返回更新过的文档而不是原始文档。默认为false也就是返回原始没更改过的文档upsert
(boolean)是否在没匹配到文档的情况新建一个(false)fields
: {Object|String} - 字段选择器等价于.select(fields).findOneAndUpdate()
sort
: 如果匹配到多个文档,设置排列顺序以选择需要更新的文档maxTimeMS
: 在查询上设置一个时间限制,要求mongoDB >= 2.6.0runValidators
如果为true,在这个命令上执行update validators。更新检验器会校验更新操作。setDefaultsOnInsert
如果这个和upsert
都置为true,Mongoose将会应用model中的默认值当新建文档时。该选项只在MongoDb>=2.4的时候才成立因为它依赖于MongoDB的$setOnInsert
操作符passRawResult
: 如果为true,从MongoDB驱动中传递原始的结果并作为回调的第三个参数context
: (string)如果设置为query
并且runValidators
打开,该值可以在更新校验运行时在自定义的校验器中引用到查询结果,如果runValidators
为false什么事也不做。
3.4.3、Query#update
申明并执行该查询作为更新的操作。提供的options:
safe
(boolean)安全模式(默认该值设为true)upsert
(boolean)是否在没匹配到文档的情况新建一个(false)multi
(boolean)是否应该更新多个文档runValidators
如果为true,在这个命令上执行update validators。更新检验器会校验更新操作。setDefaultsOnInsert
如果这个和upsert
都置为true,Mongoose将会应用model中的默认值当新建文档时。该选项只在MongoDb>=2.4的时候才成立因为它依赖于MongoDB的$setOnInsert
操作符strict
(boolean)为这次更新重写strict
选项overwrite
(boolean)关闭只更新模式,允许你重写文档(false)context
: (string)如果设置为query
并且runValidators
打开,该值可以在更新校验运行时在自定义的校验器中引用到查询结果,如果runValidators
为false什么事也不做。
具体操作范例参考Mongoose API
参考:
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 mongoose实现one-to-many的model关系 的内容有疑问,请在下面的评论系统中留言,谢谢。