Mongoose基础入门

一. 介绍

  1. MongoDB 是文档型数据库(Document Database),不是关系型数据库(Relational Database)。而Mongoose可以将MongonDB 数据库存储的文档(documents)转化为javascript 对象,然后可以直接进行数据的增删改查。
    注释:Mongoose是在node.js异步环境下对mongodb进行便捷操作的对象模型工具。Mongoose只能作为NodeJS的驱动,不能作为其他语言的驱动。
  2. Mongooose中有三个比较重要的概念,分别是SchemaModelentity。它们的关系是Schema生成ModelModel实例出entity
  3. Schema不具备操作数据库的能力。Modelentity都可对数据库操作造成影响,Modelentity更具操作性。
  4. Schema用于定义数据库的结构。每个Schema都会映射到mongodb中的一个collection
  5. Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。Model的每一个实例entity就是一个文档document
    注释: Schema对应数据库中一个集合collection里文档document的数据结构。Model对应一个集合 collectionentityModel的实例,对应集合collection中的一个文档document

二. 连接数据库

  1. 使用connect()方法连接数据库。
    语法: mongoose.connect(url, options)
    参数:options为可选参数,优先级高于urloptions可用选项如下:
选项 含义
db 数据库设置
server 服务器设置
replset 副本集设置
user 用户名
pass 密码
auth 鉴权选项
mongos 连接多个数据库
  1. 简单连接,传入url参数以及db数据库名称。
mongoose.connect('mongodb://127.0.0.1/test');

注意:默认端口为27017

  1. 传入用户名、密码、端口等参数。
mongoose.connect('mongodb://username:password@host:port/database?options...');
  1. 通过mongoose.connection监听连接状态。
var mongoose =  require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/test');

mongoose.connection.on('connected', function() {
  console.log('MongoDB connected connected');
})
mongoose.connection.on('error', function() {
  console.log('MongoDB connected error');
})
mongoose.connection.on('disconnected', function() {
  console.log('MongoDB connected disconnected');
})
  1. 避免多次连接
    新建一个mongoose.js:
var mongoose = require("mongoose");
mongoose.connect('mongodb://127.0.0.1:27017/test');
module.exports = mongoose;

每个module中,引用var mongoose = require('./mongoose.js')

  1. 断开连接
    语法:mongoose.disconnect()
mongoose.connect('mongodb://127.0.0.1:27017/test');

setTimeout(()=> {
  mongoose.disconnect();
}, 2000);

mongoose.connection.on('connected', function() {
  console.log('MongoDB connected connected');
})
mongoose.connection.on('error', function() {
  console.log('MongoDB connected error');
})
mongoose.connection.on('disconnected', function() {
  console.log('MongoDB connected disconnected');
})

三. Scheme

  1. Schema主要用于定义MongoDB中集合collection里文档document的结构。定义Schema非常简单,指定字段名和类型即可。
  2. Scheme支持以下8种类型
    String字符串、Number数字 、Date日期、Buffer二进制、Boolean布尔值、Mixed 混合类型、ObjectId对象ID、Array数组。
  3. 通过mongoose.Schema来调用Schema,然后使用new方法来创建schema对象。
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var mySchema = new Schema({
  title:  String,
  author: String,
  body:   String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs:  Number
  }
});
  1. 创建Schema对象时,声明字段类型有两种方法,一种是首字母大写的字段类型,另一种是引号包含的小写字段类型。
var mySchema = new Schema({title:String, author:String});
//或者 
var mySchema = new Schema({title:'string', author:'string'});
  1. 如果需要在Schema定义后添加其他字段,可以使用add()方法。
var MySchema = new Schema();
MySchema.add({ name: 'string', color: 'string', price: 'number' });
  1. schema中设置timestampstrueschema映射的文档document会自动添加createdAtupdatedAt这两个字段,代表创建时间和更新时间。
var UserSchema = new Schema(
  {...},
  { timestamps: true }
);
  1. 每一个文档document都会被mongoose添加一个不重复的_id_id的数据类型不是字符串,而是ObjectID类型。如果在查询语句中要使用_id,则需要使用findById语句,而不能使用findfindOne语句。

四. Model

  1. 模型Model是根据Schema编译出的构造器,或者称为类。
  2. 使用model()方法将Schema编译为Model
    语法:mongoose.model("模型名称", Scheme, "Collection名称(可选)")
    注意:如果未传第三个参数指定Collection集合名称。则Mongoose会将Collection集合名称设置为模型名称的小写版。如果名称的最后一个字符是字母,则会变成复数;如果名称的最后一个字符是数字,则不变。例如,如果模型名称为MyModel,则集合名称为mymodels;如果模型名称为Model1,则集合名称为model1
var schema = new mongoose.Schema({ num:Number, name: String, size: String});
var MyModel = mongoose.model('MyModel', schema);
  1. 生成实例entity
var schema = new mongoose.Schema({ num:Number, name: String, size: String});
var MyModel = mongoose.model('MyModel', schema);
var doc = new MyModel({ size: 'small' });
console.log(doc.size);//'small'
  1. 实例entity保存为文档document
    通过new Model()创建的实例entity,必须通过save()方法,才能将对应文档document保存到数据库的集合collection中。回调函数是可选项,第一个参数为err,第二个参数为保存的文档document对象。
    语法:save(function (err, doc) {})
var mongoose =  require('mongoose');

mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({ num:Number, name: String, size: String});
var MyModel = mongoose.model('MyModel', schema);
var doc = new MyModel({ size: 'small' });
doc.save(function (err,doc) {
  //{ __v: 0, size: 'small', _id: 5970daba61162662b45a24a1 }
  console.log(doc);
})

注释:① 一个实例Entiy对应一条文档document。② 实例entitysave方法只能够在文档document中保存scheme结构范围内的字段。

五. 自定义方法

  1. 实例方法
    Model的实例entity有很多内置方法,例如 save。可以通过Schema对象的methods属性给entity自定义扩展方法。
var schema = new mongoose.Schema({ num:Number, name: String, size: String });        
schema.methods.findSimilarSizes = function(cb){
  return this.model('MyModel').find({size:this.size},cb);
}

var MyModel = mongoose.model('MyModel', schema);
var doc1 = new MyModel({ name:'doc1', size: 'small' });
var doc2 = new MyModel({ name:'doc2', size: 'small' });
var doc3 = new MyModel({ name:'doc3', size: 'big' });
doc1.save();
doc2.save();
doc3.save();
setTimeout(function(){
    doc1.findSimilarSizes(function(err,docs){
        docs.forEach(function(item,index,arr){
            //doc1
            //doc2
            console.log(item.name)        
        })
    })  
},0) 
  1. 静态方法
    可以通过Schema对象的statics属性给 Model添加静态方法。
var schema = new mongoose.Schema({ num:Number, name: String, size: String });
schema.statics.findByName = function(name,cb){
    return this.find({name: new RegExp(name,'i')},cb);
}

var MyModel = mongoose.model('MyModel', schema);
var doc1 = new MyModel({ name:'doc1', size: 'small' });
var doc2 = new MyModel({ name:'doc2', size: 'small' });
var doc3 = new MyModel({ name:'doc3', size: 'big' });
doc1.save();
doc2.save();
doc3.save();
setTimeout(function(){
    MyModel.findByName('doc1',function(err,docs){
        //[ { _id: 5971e68f4f4216605880dca2,name: 'doc1',size: 'small',__v: 0 } ]
        console.log(docs);
    })
},0)

注释:静态方法是通过Schema对象的statics属性给model添加方法;实例方法是通过Schema对象的methods是给entity添加方法。

  1. 查询方法
    通过schema对象的query属性,给model添加查询方法。
var schema = new mongoose.Schema({ age:Number, name: String});        
schema.query.byName = function(name){
    return this.find({name: new RegExp(name)});
}

var temp = mongoose.model('temp', schema);   
temp.find().byName('huo').exec(function(err,docs){
    //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
    // { _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 } ]
    console.log(docs);
}) 

注释:查询方法需要有 .find() 作为开头。
问题:静态方法与查询方法的本质区别?

六. 新增文档

  1. mongoose提供了三种新增文档document的方法:
    (1) entitysave()方法
    (2) modelcreate()方法
    (3) modelinsertMany()方法
  2. entitysave()方法
    save([options], [options.safe], [options.validateBeforeSave], [fn])
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number});
var model = mongoose.model('temp', schema);
var entity = new model({name: 'john', age: 18});
entity.save((err, doc) => {
  //{ _id: 5b63a49e8910503426acd587, name: 'john', age: 18, __v: 0 }
  console.log(doc);
})
  1. modelcreate()方法
    使用save()方法,需要先实例化为entity,再使用save()方法保存文档document。而create()方法直接在模型model上操作,并且可以同时新增多个文档document
    Model.create(doc(s), [callback])
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number});
var model = mongoose.model('temp', schema);
model.create([{name:"lily"}, {name:'jane'}],(err, docs) => {
    console.log(docs);
    //[ { _id: 5b643cc96ca76572d64e242c, name: 'lily', __v: 0 },
    //{ _id: 5b643cc96ca76572d64e242d, name: 'jane', __v: 0 } ]
});
  1. modelinsertMany()方法
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');
var schema = new mongoose.Schema({name: String, age: Number});
var model = mongoose.model('temp', schema);
model.insertMany([{name:"a"}, {name:"b"}],(err, docs) => {
  console.log(docs);
  //[ { _id: 5b643db39d69e1731042a0f1, name: 'a', __v: 0 },
  // { _id: 5b643db39d69e1731042a0f2, name: 'b', __v: 0 } ]
});

注意:新增文档方法的callback回调函数不能使用exec方法改写。查询文档、更新文档以及删除文档方法的callback回调函数大多数都可以使用exec方法改写。
问题:modelcreate()方法与insertMany()方法的区别。

七. 查询文档

  1. mongoose提供了三种查询文档document的方法:
    (1) find()
    (2) findById()
    (3) findOne()
  2. find()方法
    Model.find(conditions, [projection], [options], [callback])
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number});
var model = mongoose.model('temp', schema);
for(let i = 1; i < 10; i++) {
  new model({name: `jake${i}`, age: i}).save();
}

//查找所有文档
model.find((err, docs) => {
  console.log(docs);//1,2,3,4,5,6,7,8,9
});

//查找年龄大于等于6的文档
model.find({age: {$gte: 6}}, (err, docs) => {
  console.log(docs); //6,7,8,9
});

//查找年龄大于等于6文档的另一种写法
model.find({age: {$gte: 6}}).exec((err, docs) => {
  console.log(docs); //6,7,8,9
});

//年龄大于8,且名字存在'jake'的数据
model.find({age: {$gt: 8}, name: /jake/}, (err, docs) => {
  console.log(docs); //9
});

//年龄等于1,且只输出'name'字段
model.find({age: {$lt: 3}}, 'name', (err, docs) => {
  console.log(docs);
  //[ { _id: 5b644b5a43048277c40834c6, name: 'jake1' },
  //{ _id: 5b644b5a43048277c40834c7, name: 'jake2' } ]
});

//年龄等于1,且不需要输出_id
model.find({age: 1}, {name: 1, _id: 0}, (err, docs) => {
  console.log(docs); //[ { name: 'jake1' } ]
});

//搜索年龄大于2,且搜索结果跳过前2条, 只留3条,且按年龄倒序
model.find({age: {$gt: 2}}).skip(2).limit(3).sort({age: -1}).exec((err, docs) => {
  console.log(docs); //7,6,5
});

注释: 可参考mongo.exe程序以及node原生查询mongodb数据库API,接口类似。
注意:① 两种写法find(..., callback)find(...).exec(callback)。② node查询通过toArray(err, docs)方法获取文档数组,mongoose查询通过exec(err, docs)方法获取文档数组。

  1. findById()方法
    Model.findById(id, [projection], [options], [callback])
model.findById('5b644b5a43048277c40834c7', (err, doc) => {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834c7, name: 'jake2', age: 2, __v: 0 }
});

//另一种写法
model.findById('5b644b5a43048277c40834c7').exec((err, doc) => {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834c7, name: 'jake2', age: 2, __v: 0 }
});

//只输出name字段
model.findById('5b644b5a43048277c40834c7', {name: 1, _id: 0}).exec((err, doc) => {
  console.log(doc);
  //{ name: 'jake2' }
});

//输出最少字段
model.findById('5b644b5a43048277c40834c7', {lean: true}).exec((err, doc) => {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834c7 }
});
  1. findOne()方法
    该方法返回查找到的所有实例的第一个。
    Model.findOne([conditions], [projection], [options], [callback])
model.findOne({age: {$gt: 5}}, (err, doc) => {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834cb, name: 'jake6', age: 6, __v: 0 }
});

model.findOne({age: {$gt: 5}}, {_id: 0}).exec((err, doc) => {
  console.log(doc);
  //{ name: 'jake6', age: 6, __v: 0 }
});

model.findOne({age: {$gt: 5}}, {name: 1}).lean().exec((err, doc)=> {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834cb, name: 'jake6' }
});
  1. 常用的查询条件如下
$or         或关系
$nor        或关系取反
$gt        大于
$gte        大于等于
$lt        小于
$lte        小于等于
$ne          不等于
$in          在多个值范围内
$nin         不在多个值范围内
$all        匹配数组中多个值
$regex      正则,用于模糊查询
$size      匹配数组大小
$maxDistance 范围查询,距离(基于LBS)
$mod      取模运算
$near      邻域查询,查询附近的位置(基于LBS)
$exists     字段是否存在
$elemMatch   匹配内数组内的元素
$within    范围查询(基于LBS)
$box       范围查询,矩形范围(基于LBS)
$center     范围醒询,圆形范围(基于LBS)
$centerSphere 范围查询,球形范围(基于LBS)
$slice      查询字段集合中的元素(比如从第几个之后,第N到第M个元素)
  1. $where操作符
    如果要进行更复杂的查询,需要使用$where操作符,$where操作符功能强大而且灵活,它可以使用任意的JavaScript作为查询的一部分,包含JavaScript表达式的字符串或者JavaScript函数。
    (1) 使用字符串
model.find({$where:"this.x == this.y"},(err,docs) => {
  console.log(docs);
  //[{ _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1},
  //{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 }]
});

model.find({$where:"obj.x == obj.y"}, (err,docs) =>{
  //[ { _id: 5972ed35e6f98ec60e3dc887,name: 'wang',age: 18,x: 1,y: 1},
  //{ _id: 5972ed35e6f98ec60e3dc889, name: 'li', age: 20, x: 2, y: 2 }]
  console.log(docs);
});

(2) 使用函数

model.find({$where:() => {
        return obj.x !== obj.y;
    }}, (err,docs) =>{
    //[ { _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
    //{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 } ]
    console.log(docs);
}) 

model.find({$where:() => {
        return this.x !== this.y;
    }},(err,docs) =>{
    //[ { _id: 5972ed35e6f98ec60e3dc886,name: 'huochai',age: 27,x: 1,y: 2 },
    //{ _id: 5972ed35e6f98ec60e3dc888, name: 'huo', age: 30, x: 2, y: 1 } ]
    console.log(docs);
}) 

八. 更新文档

更新方法

  1. 文档更新可以使用以下几种方法。
    (1) update()
    (2) updateOne()
    (3) updateMany()
    (4) find() + save()
    (5) findOne() + save()
    (6) findByIdAndUpdate()
    (7) findOneAndUpdate()
  2. update()
    第一个参数conditions为查询条件,第二个参数doc为需要修改的数据,第三个参数options为控制选项,第四个参数是回调函数。
    Model.update(conditions, doc, [options], [callback])
    options有如下选项:
  safe (boolean): 默认为true。安全模式。
  upsert (boolean): 默认为false。如果不存在则不创建新记录。
  multi (boolean): 默认为false。是否更新多个查询记录。
  runValidators: 如果值为true,执行Validation验证。
  setDefaultsOnInsert: 如果upsert选项为true,在新建时插入文档定义的默认值。
  strict (boolean): 以strict模式进行更新。
  overwrite (boolean): 默认为false。禁用update-only模式,允许覆盖记录。

(1) 只更新第一条满足条件的数据

model.update({age: {$gt: 7}}, {age: 10}, (err, row)=> {
  //{ n: 1, nModified: 1, ok: 1 }
  console.log(row);
});

//使用exec的写法
model.update({age: {$gt: 7}}, {age: 10}).exec((err, row)=> {
  //{ n: 1, nModified: 1, ok: 1 }
  console.log(row);
});

(2) 更新所有满足条件的数据

model.update({age: {$gt: 7}}, {age: 10}, {multi: true}).exec((err, row)=> {
  //{ n: 2, nModified: 1, ok: 1 }
  console.log(row);
});

(3) 如果没有符合条件的数据,则什么都不做

model.update({age: 100}, {age: 1000}).exec((err, row)=> {
  //{ n: 0, nModified: 0, ok: 1 }
  console.log(row);
});

(4) 如果设置upsert参数为true,若没有符合查询条件的文档,mongo将会综合第一第二个参数向集合插入一个新的文档。

model.update({age: 100}, {name: 'jake100'}, {upsert: true}).exec((err, row)=> {
  //{n: 1,
  // nModified: 0,
  // upserted: [ { index: 0, _id: 5b646510d22cf9feac0bd2f5 } ],
  // ok: 1 }
  console.log(row);
});

//验证插入文档
model.find({age: '100'}).exec((err, docs) =>{
  console.log(docs)
  //[ { _id: 5b646510d22cf9feac0bd2f5,
  //     age: 100,
  //     __v: 0,
  //     name: 'jake100' } ]
});

注意:update()方法中的回调函数不能省略,否则数据不会被更新。如果无需在回调函数中做进一步操作,则可以使用exec()简化代码。
例如:temp.update({name:/aa/},{age: 0},{upsert:true}).exec();

  1. updateOne()
    updateOne()方法只能更新找到的第一条数据,即使设置{multi:true}也无法同时更新多个文档。
model.updateOne({age: {$gt: 8}}, {name: 'jake80'}).exec((err, res)=> {
  //{ n: 1, nModified: 1, ok: 1 }
  console.log(res);
});
  1. updateMany()
    updateMany()update()方法唯一的区别就是默认更新多个文档,即使设置{multi:false}也无法只更新第一个文档。
    Model.updateMany(conditions, doc, [options], [callback])
model.updateMany({age: {$gt: 8}}, {name: 'jake80'}).exec((err, res)=> {
  //{ n: 3, nModified: 3, ok: 1 }
  console.log(res);
});
  1. find() + save()
    如果需要更新的操作比较复杂,可以使用find()+save()方法来处理。
model.find({age: {$gt: 8}}).exec((err, docs)=> {
  docs.forEach(doc => {
    doc.name = `jake${doc.age}`;
    doc.save();
  });
  console.log(docs);
  //[ { _id: 5b644b5a43048277c40834cd, name: 'jake10', age: 10, __v: 0 },
  //{ _id: 5b644b5a43048277c40834ce, name: 'jake10', age: 10, __v: 0 },
  //{ _id: 5b646510d22cf9feac0bd2f5, age: 100, __v: 0, name: 'jake100' } ]
});
  1. findOne() + save()
    如果需要更新的操作比较复杂,可以使用findOne()+save()方法来处理。
model.findOne({age: 10}).exec((err, doc)=> {
  doc.name = 'jake9';
  doc.age = 9;
  doc.save();
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834cd, name: 'jake9', age: 9, __v: 0 }
});
  1. findByIdAndUpdate()
    Model.findOneAndUpdate([conditions], [update], [options], [callback])
  2. findOneAndUpdate()
    Model.findOneAndUpdate([conditions], [update], [options], [callback])

修改器

数据准备工作,创建集合及文档数据如下:

var mongoose =  require('mongoose');
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({
  name: String,
  age: Number,
  array: [Number]
});
var Model = mongoose.model('temp', schema);
for(let i = 1; i < 10; i++) {
  new Model({name: `jake${i}`, age: i, array: []
 }).save();
}

对象修改器

  1. $inc 增减修改器,只对数字有效。
    找到age等于1的文档,修改age字段值,自减5。
Model.update({age: 1}, {$inc: {age: -5}}).exec()
  1. $set 指定一个键的值
Model.update({name: 'jake1'}, {$set: {age: 2}}).exec()
  1. $unset删除一个键
Model.update({name: 'jake1'}, {$unset: {age: ''}}).exec()

注意:$unset操作符只匹配keyvalue可以是任意值。

数组修改器

  1. $push数组尾部插入。
    给匹配文档的array键对应数组插入数字1。
Model.update({age: 2}, {$push: {array: 1}}).exec();
  1. $addToSet数组尾部插入,如果存在则不插入。
Model.update({age: 2}, {$addToSet: {array: 1}}).exec();
  1. $pop 数组尾部删除。
    传入1删除数组尾元素,传入-1删除数组首元素。
Model.update({age: 2}, {$pop: {array: 1}}).exec();
  1. $pull删除数组指定元素。
Model.update({age: 2}, {$pull: {array: 6}}).exec();

九. 删除文档

  1. 有三种方法用于文档删除。
    (1) remove()
    (2) findOneAndRemove()
    (3) findByIdAndRemove()
    注意:这些方法中的回调函数不能省略,否则数据不会被删除。当然,可以使用exec()方法来简写代码。
  2. remove()
    remove有两种形式,一种是Modelremove()方法,一种是documentremove()方法。
    (1) Modelremove()方法
    该方法的第一个参数conditions为查询条件,第二个参数为回调函数。
    model.remove(conditions, [callback])
model.remove({age: {$gte:9}}, (err, res) => {
  console.log(res);
  //{ n: 3, ok: 1 }
});

//使用exec的写法
model.remove({age: {$gte:9}}).exec((err, res) => {
  console.log(res);
  //{ n: 0, ok: 1 }
});

(2) documentremove()方法
document.remove([callback])

model.findOne({age: 8}).exec((err, doc) => {
    doc.remove((err, doc) => {
      console.log(doc);
      //{ _id: 5b64fdfa1bfab0852697bc00, name: 'jake8', age: 8, __v: 0 }
    });
});

注释:①modelremove()方法回调可以使用exec()方法改写, documentremove()方法不可以。②modelremove()方法删除符合条件的所有document文档,documentremove()方法删除当前文档。

  1. findOneAndRemove()
    modelremove()会删除符合条件的所有数据,如果只删除符合条件的第一条数据,则可以使用modelfindOneAndRemove()方法。
    Model.findOneAndRemove(conditions, [options], [callback])
model.findOneAndRemove({age: {$gte: 0}}, (err, doc) => {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834c6, name: 'jake1', age: 1, __v: 0 }
});

model.findOneAndRemove({age: {$gte: 0}}).exec((err, doc) => {
  console.log(doc);
  //{ _id: 5b644b5a43048277c40834c7, name: 'jake2', age: 2, __v: 0 }
});
  1. findByIdAndRemove()
    Model.findByIdAndRemove(id, [options], [callback])
model.find().exec((err, docs) => {
  const docIdArr = docs.map(doc => doc._id);
  model.findByIdAndRemove(docIdArr[0]).exec((err, doc) => {
    console.log(doc);
    //{ _id: 5b644b5a43048277c40834c8, name: 'jake3', age: 3, __v: 0 }
  })
});

十. Promise

  1. Mongoose异步操作,例如.save()方法,会返回一个ES6标准的promises。你可以使用类似 MyModel.findOne({}).then()await MyModel.findOne({}).exec()的写法。
var gnr = new Band({
  name: "Guns N' Roses",
  members: ['Axl', 'Slash']
});

var promise = gnr.save();
assert.ok(promise instanceof Promise);

promise.then(function (doc) {
  assert.equal(doc.name, "Guns N' Roses");
});
  1. mongoose queries 查询操作虽然有then方法,但并不是一个完全的promise。可以使用exec()方法将其转化为一个完全的promise
var query = Band.findOne({name: "Guns N' Roses"});
assert.ok(!(query instanceof Promise));

// A query is not a fully-fledged promise, but it does have a `.then()`.
query.then(function (doc) {
  // use doc
});

// `.exec()` gives you a fully-fledged promise
var promise = query.exec();
assert.ok(promise instanceof Promise);

promise.then(function (doc) {
  // use doc
});
  1. 可以通过重写mongoose.Promise的方式使用第三方promise库,例如 bluebird
var query = Band.findOne({name: "Guns N' Roses"});

// Use bluebird
mongoose.Promise = require('bluebird');
assert.equal(query.exec().constructor, require('bluebird'));

十一. 前后钩子

  1. 前后钩子即pre()post()方法,又称为中间件,是在执行某些操作时可以执行的函数。中间件在schema上指定,类似于静态方法或实例方法等。
    注意:①前后钩子方法定义在schema上。② 前后钩子方法必须在Model创建之前定义,否则不生效。
  2. 可以在model执行下列操作时,设置前后钩子。
    init validate save remove count find findOne findOneAndRemove findOneAndUpdate insertMany update
  3. pre()中间件
    find()方法为例,在执行find()方法之前,执行pre()方法。
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number});

schema.pre('find', (next) => {
  console.log('pre hook fu1');
  next();
});

schema.pre('find', (next) => {
  console.log('pre hook fu2');
  next();
});

var Model = mongoose.model('temp', schema);

Model.find((err, docs) => {
  console.log(docs[0]);
  //pre hook fu1
  //pre hook fu2
  //{ _id: 5b644b5a43048277c40834c9, name: 'jake4', age: 4, __v: 0 }
});
  1. post()中间件
    post()方法并不是在执行某些操作后再去执行的方法,而在执行某些操作前最后执行的方法,post()方法里不可以使用next()
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number});

schema.post('find', (next) => {
  console.log('post hook fu1');
});

schema.post('find', (next) => {
  console.log('post hook fu2');
});

var Model = mongoose.model('temp', schema);

Model.find((err, docs) => {
  console.log(docs[0]);
  //post hook fu1
  //post hook fu2
  //{ _id: 5b644b5a43048277c40834c9, name: 'jake4', age: 4, __v: 0 }
});

十二. 查询后处理

  1. 常用的查询后处理的方法如下所示
  • sort 排序
  • skip 跳过
  • limit 限制
  • select 显示字段
  • exect 执行
  • count 计数
  • distinct 去重
  1. 方法示例
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number});
var Model = mongoose.model('temp', schema);

for(let i = 1; i <= 15; i++) {
  new Model({name: `jake${i}`, age: i}).save();
}

Model.find((err, docs) => {
  console.log(docs); //1 - 15
});

//sort 排序
Model.find().sort({'age': -1}).exec((err, docs) => {
  console.log(docs); //15 - 1
});

//skip 跳过
Model.find().skip(3).exec((err, docs) => {
  console.log(docs); //4 - 15
});

//limit 限制
Model.find().limit(2).exec((err, docs) => {
  console.log(docs); //1 - 2
});

//select 限制字段
Model.find().select({name: 1, _id: 0}).exec((err, docs) => {
  console.log(docs[0]); //{ name: 'jake2' }
});

//链式操作
Model.find().sort({'age': -1}).skip(2).limit(1).select({age: 1, _id: 0}).exec((err, docs) => {
  console.log(docs); //[ { age: 13 } ]
});

//count显示文档数目
Model.find().count().exec((err, count) => {
  console.log(count); //15
});

//distinct 去重
Model.find().distinct('name').exec((err, arr) => {
  console.log(arr); //jake1-jake15
});

十三. 文档验证

  1. 如果不进行文档验证,保存文档时,就可以不按照Schema设置的字段进行设置,分为以下几种情况。
var mongoose =  require('mongoose');
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var schema = new mongoose.Schema({name: String, age: Number,x: Number, y: Number});
var Model = mongoose.model('temp', schema);

(1) 缺少字段的文档也可以保存成功

new Model({age: 10}).save((err, doc) => {
  console.log(doc);
  //{ _id: 5b65862905b74ea05e211322, age: 10, __v: 0 }
});

(2) 包含未设置的字段的文档也可以保存成功,未设置的字段不被保存。

new Model({age: 11, z: 10}).save((err, doc) => {
  console.log(doc);
  //{ _id: 5b65865ca0864ba06fb02d37, age: 11, __v: 0 }
});

(3) 包含字段类型与设置不同的文档可以保存成功,不同字段类型的字段被保存为设置的字段类型

new Model({age:true,name:10}).save(function(err,doc){
  //{ _id: 5b6586b547589ba082d19c3c, age: 1, name: '10', __v: 0 }
  console.log(doc);
});
  1. 通过文档验证,就可以避免以上几种情况发生。文档验证在SchemaType中定义,格式如下。
    {name: {type:String, validator:value}}
    常用验证包括以下几种:
    required: 数据必须填写
    default: 默认值
    min: 最小值(只适用于数字)
    max: 最大值(只适用于数字)
    match: 正则匹配(只适用于字符串)
    enum: 枚举匹配(只适用于字符串)
    validate: 自定义匹配
  2. required文档验证
    age设置为必填字段,如果没有age字段,文档将不被保存,且出现错误提示。
var schema = new mongoose.Schema({age:{type:Number,required:true}, name: String,x:Number,y:Number});
var Model = mongoose.model('temp', schema);
new Model({name:"abc"}).save((err,doc) => {
  //Path `age` is required.
  console.log(err.errors['age'].message);
});
  1. default文档验证
    设置age字段的默认值为18,如果不设置age字段,则会取默认值。
var schema = new mongoose.Schema({ age:{type:Number,default:18}, name:String,x:Number,y:Number});
var Model = mongoose.model('temp', schema);
new Model({name:'a'}).save((err,doc) => {
  //{ __v: 0, name: 'a', _id: 59730d2e7a751d81582210c1, age: 18 }
  console.log(doc);
});
  1. min、max文档验证
    age的取值范围设置为[0,10]。如果age取值为20,文档将不被保存,且出现错误提示。
var schema = new mongoose.Schema({ age:{type:Number,min:0,max:10}, name: String,x:Number,y:Number});
var Model = mongoose.model('temp', schema);
new Model({age:20}).save((err,doc) => {
  //Path `age` (20) is more than maximum allowed value (10).
  console.log(err.errors['age'].message);
});
  1. match文档验证
    namematch设置为必须存在'a'字符。如果name不存在'a',文档将不被保存,且出现错误提示。
var schema = new mongoose.Schema({ age:Number, name:{type:String,match:/a/},x:Number,y:Number});
var Model = mongoose.model('temp', schema);
new Model({name:'bbb'}).save((err,doc) => {
  //Path `name` is invalid (bbb).
  console.log(err.errors['name'].message);
});
  1. enum文档验证
    name的枚举取值设置为['a','b','c'],如果name不在枚举范围内取值,文档将不被保存,且出现错误提示。
var schema = new mongoose.Schema({ age:Number, name:{type:String,enum:['a','b','c']},x:Number,y:Number});
var Model = mongoose.model('temp', schema);
new Model({name:'bbb'}).save((err,doc) => {
  //`bbb` is not a valid enum value for path `name`.
  console.log(err.errors['name'].message)
});
  1. validate文档验证
    validate实际上是一个函数,函数的参数代表当前字段的值,返回true表示通过验证,返回false表示未通过验证。利用validate可以自定义任何条件。
    例如,定义名字name的长度必须在4个字符以上。
var schema = new mongoose.Schema({ 
  name:{
    type: String, 
    validate: value =>value.length > 4
  }, 
  age: Number,
  x: Number,
  y: Number
});
var Model = mongoose.model('temp', schema);
new Model({name:'abc'}).save((err, doc) => {
  //Validator failed for path `name` with value `abc`
  console.log(err.errors['name'].message);
});

十四. population连表操作

  1. population介绍
    (1) MongoDB是文档型数据库,所以它没有关系型数据库joins(数据库的两张表通过"外键"建立连接关系) 特性。在建立数据的关联时会比较麻烦。为了解决这个问题,Mongoose封装了一个population功能。使用population可以实现在一个 document中填充其他 collection(s)document(s)
    (2) 在定义schema的时候,如果设置某个 field 关联另一个schema,那么在获取 document 的时候就可以使用 population 功能通过关联schemafield 找到关联的另一个 document,并且用被关联 document 的内容替换掉原来关联字段(field)的内容。
  2. 连表关系场景
    场景:用户user可以写文章post,并且对文章post进行评论comment
    分析:一个用户user可以写多篇文章post。一篇文章post只能有一个作者user,但可以有多条评论comment。一条评论comment 只属于一篇文章post,且只属于一个用户user
    示例:用户A写了文章A、评论A,用户B写了文章B、评论B,用户C写了文章C、评论C;用户A在文章B上添加了评论A,用户B在文章C上添加了评论B,用户C在文章A上添加了评论C
  3. 连表关系示例代码
    (1) 创建用户、文章、评论三个schemaModel
var mongoose =  require('mongoose');
mongoose.connect('mongodb://test:test@127.0.0.1:27017/test');

var Schema = mongoose.Schema;

//创建用户scheme以及model
var userSchema = new Schema({
  name  : String,
  posts : [{ type: Schema.Types.ObjectId, ref: 'post' }]
});
var UserModel = mongoose.model('user', userSchema);

//创建文章scheme以及model
var postSchema = new Schema({
  name  : String,
  poster   : { type: Schema.Types.ObjectId, ref: 'user' },
  comments : [{ type: Schema.Types.ObjectId, ref: 'comment' }],
});
var PostModel = mongoose.model('post', postSchema);

//创建评论scheme以及model
var commentSchema = new Schema({
  name  : String,
  post      : { type: Schema.Types.ObjectId, ref: "post" },
  commenter : { type: Schema.Types.ObjectId, ref: 'user' },
});
var CommentModel = mongoose.model('comment', commentSchema);
  • 创建了三个 ModelUserModelPostModelCommentModel
  • UserModel 的属性 posts对应是一个 ObjectId 的数组,ref表示关联PostModel
  • PostModel的属性 postercomments 分别关联UserModelCommentModel
  • CommentModel的属性 postcommenter 分别关联PostModelUserModel
    注意:① ref指向mongoose.model(name, schema);方法的name参数,而不是方法返回值model。②userSchema中应该保存而未保存comments数组。
    (2) 创建entity实例、建立关系并保存数据
//创建三个用户userA、userB、userC
var userA = new UserModel({name: 'userA'});
var userB = new UserModel({name: 'userB'});
var userC = new UserModel({name: 'userC'});
//创建三篇文章postA、postB、postC
var postA = new PostModel({name:  'postA'});
var postB = new PostModel({name:  'postB'});
var postC = new PostModel({name:  'postC'});
//创建三个评论commentA、commentB、commentC
var commentA = new CommentModel({name: 'commentA'});
var commentB = new CommentModel({name: 'commentB'});
var commentC = new CommentModel({name: 'commentC'});

//建立用户与文章的关系
userA.posts.push(postA._id);
//moongoose封装的语法糖,与userA.posts.push(postA)写法含义相同
userB.posts.push(postB);
userC.posts.push(postC);
//建立文章与用户、评论的关系
postA.poster = userA;
postB.poster = userB;
postC.poster = userC;
postA.comments.push(commentC);
postB.comments.push(commentA);
postC.comments.push(commentB);
//建立评论与用户、文章的关系
commentA.post = postB;
commentB.post = postC;
commentC.post = postA;
commentA.commenter = userA;
commentB.commenter = userB;
commentC.commenter = userC;

//保存数据
userA.save();
userB.save();
userC.save();
postA.save();
postB.save();
postC.save();
commentA.save();
commentB.save();
commentC.save();
  1. population连表操作
    (1) query.populate
    语法:query.populate(path, [select], [model], [match], [options])
    pathString | Object ;指定要填充的关联字段。
    selectObject | String;指定填充 document中的哪些字段。
    modelModel;指定关联字段的model,若未指定则使用Schemaref
    matchObject;指定附加的查询条件。
    optionsObject;指定附加的其他查询选项,如排序以及条数限制等。
UserModel.find().skip(1).limit(1)
  .populate('posts', 'name')
  .exec((err, docs) => {
    console.log(docs[0].posts);
    //[{"_id":"5b6655ac4f1303b2933a7dc0","name":"postB"}]
});

UserModel.findOne({name: 'userC'})
  .populate({
    path: 'posts',
    select: { name: 1, _id: 0}
  })
  .exec((err, doc) => {
    console.log(doc.posts); //[{"name":"postC"}]
  });

PostModel.findOne({name: 'postA'})
  .populate('poster comments', 'name -_id')
  .exec((err, doc)=> {
    console.log(doc.poster); //{ name: 'userA' }
    console.log(doc.comments); //[{"name":"commentC"}]
  });

PostModel.findOne({name: 'postC'})
  .populate([
    {path: 'poster', select: 'name'},
    {path: 'comments', select: {_id: 0}}
  ])
  .exec((err, doc) => {
    console.log(doc.poster);
    //{ _id: 5b6655ac4f1303b2933a7dbe, name: 'userC' }
    console.log(doc.comments);
    //[{"name":"commentB","post":"5b6655ac4f1303b2933a7dc1","commenter":"5b6655ac4f1303b2933a7dbd","__v":0}]
  });

(2) Model.populate
语法:Model.populate(docs, options, [cb(err,doc)])

CommentModel.findOne((err, doc) => {
  CommentModel.populate(doc, {path: 'post commenter', select: 'name'}, (err, doc) => {
    console.log(doc.post);
    //{ _id: 5b6655ac4f1303b2933a7dc0, name: 'postB' }
    console.log(doc.commenter);
    //{ _id: 5b6655ac4f1303b2933a7dbc, name: 'userA' }
  })
});

(3) document.populate
语法:Document.populate([path], [callback])

CommentModel.findOne((err, doc) => {
  doc.populate({path: 'post commenter', select: 'name'}, (err, doc) => {
    console.log(doc.post);
    //{ _id: 5b6655ac4f1303b2933a7dc0, name: 'postB' }
    console.log(doc.commenter);
    //{ _id: 5b6655ac4f1303b2933a7dbc, name: 'userA' }
  })
});

十五. 参考资料

Mongoose官网
Mongoose Promise语法
Mongoose基础入门
Mongoose 之 Population 使用
Mongoose 使用之 Population

推荐阅读更多精彩内容

  • Mongoose 是什么?一般我们不直接用MongoDB的函数来操作MongoDB数据库 Mongose就是一套操...
    独孤久见阅读 141评论 0 1
  • mongoose入门 MongoDB是一个开源的NoSQL数据库,相比MySQL那样的关系型数据库,它更显得轻巧、...
    huilegezai阅读 1,777评论 0 11
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 15,577评论 1 69
  • 链表反转的思路:1.利用栈后进先出的特性,将链表的每个节点都Push进栈,然后再Pop出栈,保存进链表,实现反转。...
    李泽兴阅读 54评论 0 0
  • 有一种文章逻辑叫SCQA(场景-冲突-问题-答案) 一秒钟:长虫(蛇)吻妲己
    花拳绣腿阅读 32评论 0 0