http://mongoosejs.com/docs/api.html#index-js
mongoose是nodejs环境下操作mongodb的模块封装,使用mongoose之后,实际上只需要在mongodb中创建好数据库与用户,集合的定义、创建、操作等直接使用mongoose即可。
- 一、连接
- 二、重要概念
- 三、基本操作
- 1、Schema
- 2、Model
- 3、实例化document
- 4、保存数据
- 5、文档查询
- 6、文档更新
- 7、文档删除
- 8、自定义方法
- 9、虚拟属性
- 10、前置与后置钩子
一、连接
let mongoose = require('mongoose');//连接mongodb
//非auth模式
//mongoose.connect('mongodb://localhost:27017/mall');
//auth模式
/**
mongodb 为协议
第一个mall: 连接数据库的用户名
123456: 用户的密码
localhost: mongodb地址
27017: mongodb端口号
第二个mall: 数据库名字
**/
mongoose.connect('mongodb://mall:123456@localhost:27017/mall');//连接失败
mongoose.connection.on('connected',() => {console.log('mongodb connected!');
});//连接成功
mongoose.connection.on('error',(err) => {console.log('mongodb connect fail:'+err);
});//连接断开
mongoose.connection.on('disconnected',() => {console.log('mongodb connect disconnected!');
});// connection的事件列表可查看:http://mongoosejs.com/docs/api.html#connection_Connection
// 或 ./node_modules/mongoose/lib/connection.js#Connection()// 关闭的两种方式
// mongoose.connection.close(); 等同于 db.close();
// mongoose.disconnect();
二、重要概念
Mongooose中,有三个比较重要的概念,Schema、Model、Document。Schema生成Model,Model实例化成为Document。
Schema用于定义数据库的结构。类似创建表时的数据定义(不仅仅可以定义文档的结构和属性,还可以定义文档的实例方法、静态模型方法、复合索引等),每个Schema会映射到mongodb中的一个collection,Schema不具备操作数据库的能力。
Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。Model的每一个实例(instance)就是一个文档document。
Document是new Model后创建的实例,它的操作会影响数据库。
三、基本操作
1、Schema
Schema是对mongodb中某个集合的结构描述,它定义一个集合中应该包含哪些数据项,每个数据项的数据类型与检查约束。可以想象成一个抽象类,只有定义,没有实现。就是规定好某个集合的框架应该是怎么样的。也可以理解是mysql中的表结构。
可以为数据项指定的数据类型有8种:
String 字符串
Number 数字
Date 日期
Buffer 二进制
Boolean 布尔值
Mixed 混合类型
ObjectId 对象ID
Array 数组
创建Schema
const mongoose = require('mongoose')
//获取Schema
const Schema = mongoose.Schema;//声明一个Schema实例,实际就是创建一个集合的结构框架
let animalSchema = new Schema({title: String, //类型可以是首字母大写age: 'number', //类型也可以写成字符串全小写food: [{ //数组name: String}]
});//如果需要为animalSchema增加属性,可以使用add
animalSchema.add({sex: {type: String,enum: ['male', 'female'] //规定,字段值为枚举约束,只能填male/female}
});
创建Schema时可以有一系列的约束条件,类似mysql中字段的非空、唯一等约束。
基本语法是:
{name: {type:String, validator:value}}
常用的约束有:
required: 数据必须填写
default: 默认值
min: 最小值(只适用于数字)
max: 最大值(只适用于数字)
match: 正则匹配(只适用于字符串)
enum: 枚举匹配(只适用于字符串)
validate: 自定义匹配
其中validate是自定义约束
例如要自定义检查长度
// 自定义长度检查
let lengthValidate = (param) => {return param.length <= 10
};//如果需要为animalSchema增加属性,可以使用add
animalSchema.add({sex: {type: String,enum: ['male', 'female'], //规定,字段值为枚举约束,只能填male/femalevalidate: lengthValidate}
});
2、Model
model是根据Schema定义的结构框架生成一个映射mongodb的数据模型,可以把它理解是一个类,是根据Schema创建的一个类,他的数据结构是依据Schema生成的,这个类用来生成document实例。
//通过schema构建model,第一个参数是给model起个名字,第二个参数是构造这个model所依据的schema
let animalModel = mongoose.model('animalModel', animalSchema);
3、实例化document
document是相当于是mongodb中的每一条数据了,它是通过new一个model得到的。
// 通过实例化model得到document,document的数据项需根据schema定义的结构来写// 通过实例化model得到document,document的数据项需根据schema定义的结构来写let cat = new animalModel({title: '猫咪',food: [{name: '鱼'},{name: '火腿'}],sex: 'male'});
到这一步,mongodb中还没有animalMode集合以及数据。
4、保存数据
- 1、save()
在mongoose中保存数据,实际就是保存某个document,document通过model实例化来创建。document的save()方法可以将document映射保存到mongodb中,save()方法中可以传递一个回调函数作为参数。因为是在document上调用,自然一次只保存一条文档。
方法原型:
save(function (err, document) {}) //回调函数中第二个参数是使用保存的数据形成的对象
保存cat:
//保存document,映射成为一条记录
cat.save((err, catObj) => {if (err) {console.log('保存失败');console.info(err);} else {console.info(catObj);}
});
保存后打印:
{ title: '猫咪',food: [ { name: '鱼', _id: 5a96555e95f678530054f918 },{ name: '火腿', _id: 5a96555e95f678530054f917 } ],sex: 'male',_id: 5a96555e95f678530054f919,__v: 0 }
保存后这个回调中的第二个参数就被填充好值,其中的主键_id也在其中,__v是mongoose用来管理数据版本号的标识,自己不用动。
这时看mongodb中已有集合,集合中已经有一条数据(文档)了。
注意:
1、从上图可以看出来,集合的名字是根据mongoose.model('animalModel', animalSchema)
中第一个参数来的,变成小写且复数形式,如果结尾是数字则不变。比如这个地方如果第一个参数给的是’animal’,则生成的集合名称就应该是’animals’。
2、在mongoose中操作mongodb,因为有了schema、model、ducument的概念,就不是像mysql一样,事先创建好表结构,而是根据数据结构设计,在代码中声明好schema,再生成model、创建document,进行数据操作。
- 2、create()
和save()不一样的是,create()不是在document上调用的,而是在model上调用的,并且可以一次保存多条文档。
接着用上面的animalModel,这次不去new它了,也就是不去操作document了,直接用mongoose.model出来的model。同时保存两条狗。
let dog1 = {title: '1号狗狗',food: [{name: '骨头'},{name: '肉肉'},],sex: 'male'};let dog2 = {title: '2号狗狗',food: [{name: '骨头'},{name: '肉肉'},],sex: 'male'};animalModel.create(dog1, dog2, (err, rsObj1, rsObj2) => {if (err) {console.log('保存失败');console.info(err);} else {console.log('保存成功');console.info(rsObj1);console.info(rsObj2);}});
打印的结果:
保存成功
{ title: '1号狗狗',food: [ { name: '骨头', _id: 5a96607b5ce650532494f010 },{ name: '肉肉', _id: 5a96607b5ce650532494f00f } ],sex: 'male',_id: 5a96607b5ce650532494f011,__v: 0 }
{ title: '2号狗狗',food: [ { name: '骨头', _id: 5a96607b5ce650532494f013 },{ name: '肉肉', _id: 5a96607b5ce650532494f012 } ],sex: 'male',_id: 5a96607b5ce650532494f014,__v: 0 }
再看看mongodb中:
注意:这里也验证了一个问题,就是如果model对应的集合已经存在于mongodb中了,则会直接往这个集合中添加文档,如果还没有,则会创建集合。所以在上面保存猫咪的时候,同时创建了集合与添加猫咪文档,而这里的狗狗,直接添加文档。
- 3、insertMany()
insertMany()也是model的方法,一次插入多条数据,注意回调中的返回值是所有结果对象的数组。
let monkey1 = {title: '1号猴猴',food: [{name: '香蕉'},{name: '桃'},],sex: 'male'};let monkey2 = {title: '2号猴猴',food: [{name: '香蕉'},{name: '桃'},],sex: 'male'};animalModel.insertMany([monkey1, monkey2], (err, rsArr) => {if (err) {console.log('保存失败');console.info(err);} else {console.log('保存成功');console.info(rsArr);}});
打印结果:
保存成功
[ { title: '1号猴猴',food: [ [Object], [Object] ],sex: 'male',_id: 5a967c3f44d6d753b6cd6354,__v: 0 },{ title: '2号猴猴',food: [ [Object], [Object] ],sex: 'male',_id: 5a967c3f44d6d753b6cd6357,__v: 0 } ]
5、文档查询
mongodb中的数据查询就是查文档。
- 1、find()
find()是用在model上的,这一点很容易理解,既然是查询数据,那么当前肯定是不明确要得到的文档结果是怎样的,方法也就不会在document上。
find()方法在官方文档中的描述是:
Model.find()Parameters [callback] «Function»
Returns:«Query»Finds documents
The conditions are cast to their respective SchemaTypes before the command is sent.Example:
// named john and at least 18
MyModel.find({ name: 'john', age: { $gte: 18 }});// executes immediately, passing results to callback
MyModel.find({ name: 'john', age: { $gte: 18 }}, function (err, docs) {});// name LIKE john and only selecting the "name" and "friends" fields, executing immediately
MyModel.find({ name: /john/i }, 'name friends', function (err, docs) { })// passing options
MyModel.find({ name: /john/i }, null, { skip: 10 })// passing options and executing immediately
MyModel.find({ name: /john/i }, null, { skip: 10 }, function (err, docs) {});// executing a query explicitly
var query = MyModel.find({ name: /john/i }, null, { skip: 10 })
query.exec(function (err, docs) {});// using the promise returned from executing a query
var query = MyModel.find({ name: /john/i }, null, { skip: 10 });
var promise = query.exec();
promise.addBack(function (err, docs) {});
官方示例find的用法非常清晰了,find方法最简的参数是只给一个回调函数,表示查询所有结果,相当于是sql中的select * from 表名。
find()方法返回值是query对象,query对象有exec和一系列方法,exec()可以执行准备好的find()查询,回调得到结果。
回到animal示例中:
查询animalmodels集合中所有的文档:
//方式一:
animalModel.find((err, rs) => {if (err) {console.log('查询失败');console.info(err);} else {console.log('查询成功');console.info(rs);}});//方式二:let animalQuery = animalModel.find();animalQuery.exec((err, rs) => {if (err) {console.log('查询失败');console.info(err);} else {console.log('查询成功');console.info(rs);}});//两种方式的结果是一样的,返回的rs是一个包含集合中所有文档的数组
find()的完整参数列表是:
Model.find([查询条件], [返回字段列表], [设置选项], [回调函数]);
对于第一个参数,比如age: { $gte: 18 },表示要查询年龄大于等于18的文档。常用的条件操作符有:
$lt、$lte、$gt、$gte、$in、$nin、$eq、$ne、$or(model.find({$or:[{ color: 'red' }, { status: 'emergency' }]}))、$and、$not、$nor、$exits(值是否存在,model.find({name:{$exits:true}}))、$all(通常用来匹配数组里面的键值,匹配多个值(同时具有) $all:[“apple”,“banana”,“peach”]})、$size(用来查询数组的长度值 model.find({name:{$size:3}}); 匹配name的数组长度为3)
有一个特殊的操作符$where
,可以灵活的设置查询条件,$where
后可以直接写任何的js作为查询条件,但实际上完全可以使用model.where()方法来代替,where()会更加方便。
{$where:"this.x == this.y"}
{$where:function(){return obj.x !== obj.y;
}}
这些操作符在find的第一个参数中使用,query中也有一一对一个的方法。
对于第二个参数,可以设置要返回的字段列表,{name:1,_id:0}表示返回name字段,不返回_id字段。
对于第三个参数,官方示例中用到了一个skip,还有limit、sort。
除了find()外,还有findById()、findOne(),与find的用法都是一样的。只不过findById的第一个参数是准确的id值。这两个方法都只返回一条文档。
- 2、where()
官网上这么介绍的
Model.where()
Parameters [val] «Object» optional valueReturns:«Query»
Creates a Query, applies the passed conditions, and returns the Query.
官方示例:
User.find({age: {$gte: 21, $lte: 65}}, callback);
等同于
User.where('age').gte(21).lte(65).exec(callback);
因为where返回的是query对象,所以可以链式调用:
User
.where('age').gte(21).lte(65)
.where('name', /^b/i)
... etc
所以在实际的使用过程中,用query对象来进行查询是比较方便的,用find()的返回值query链式调用其他方法,或者用where()的返回值query链式调用其他方法,最后用exec(callback)得到结果,这样比较清晰。query有一系列的方法,异步官方文档。
- 3、query常用方法
sort 排序
skip 跳过
limit 限制
select 显示字段
exect 执行
count 计数
distinct 去重
关于分页与排序,query的三个方法:
query.limit(20); //只返回前20个内容
query.skip(2); //跳过多少条,实际就是开始的索引
query.sort({name:1,age:-1}); //1升序,2降序
6、文档更新
- 1、update()
Model.update(conditions, doc, [options], [callback])
第一个参数conditions为查询条件,第二个参数doc为需要修改的数据,第三个参数options为控制选项,第四个参数是回调函数。
options有以下选项:
safe (boolean): 默认为true。安全模式。upsert (boolean): 默认为false。如果不存在则创建新记录。multi (boolean): 默认为false。是否更新多个查询记录。runValidators: 如果值为true,执行Validation验证。setDefaultsOnInsert: 如果upsert选项为true,在新建时插入文档定义的默认值。strict (boolean): 以strict模式进行更新。overwrite (boolean): 默认为false。禁用update-only模式,允许覆盖记录。
把名字叫猫咪的文档的名字更新成猫猫:
animalModel.update({title :{$eq: '猫咪'}}, {title: '猫猫'}, (err, rs) => {if (err) {console.log('更新失败');console.info(err);} else {console.log('更新成功');console.info(rs);}});
如果不设置multi的话,即使有多条符合条件的文档,也只更新一条。
如果设置options里的upsert参数为true,若没有符合查询条件的文档,mongo将会综合第一第二个参数向集合插入一个新的文档。
2、updateMany()
updateMany()与update()的区别是更新多个文档,即使设置{multi:false}也更新多个。3、updateOne()
updateOne()只更新找到的第一条数据,即使设置{multi:true}只更新一个。4、数组更新
update第二个参数,实际可以写成{$set: {title: '猫猫'}}
,$set
操作符表示设值,如果要更新的是数组,可以有以下操作符:
$addToSet // 当且仅当待添加到数组的元素是当前数组所没有时,才会去添加
$pop // 当指定数组的修饰符值为-1时,删除该数组的第1个元素;当值为1时,删除最后一个元素
$pull // 删除指定查询条件下的数组元素
$pullAll // 删除数组中符合给定的集合的值的所有元素,与$pull的区别于目标删除元素是被逐一列举出来的
$push // 将元素添加到指定的数组中
给猫添加一个食物:
animalModel.update({title :{$eq: '喵星人'}},{$addToSet:{food: {name: '蛋黄派'}}},(err,rs) =>{if (err) {console.info("保存失败");console.info(err);} else {console.info("保存成功");console.info(rs);}});
结果:
保存成功
{ n: 1, nModified: 1, ok: 1 }
- 5、其他
关于其他更新方法还有findOneAndUpdate()、findByIdAndUpdate()。
通过查询+保存操作也可以实现更新,查询的结果实际是一个document对象,调用save方法,猜想是通过_id与_v来进行的比对,将直接更新本条记录。
animalModel.where('title').eq('猫猫').exec((err, rs) => {if (err) {console.info(err);} else {console.info(rs);rs.forEach(function(item,index,array){item.title = "喵星人";item.save((err,rs) =>{if (err) {console.info("保存失败");console.info(err);} else {console.info("保存成功");console.info(rs);}});});}});
7、文档删除
- 1、remove()
remove方法存在两个位置,一个是model上,一个是ducument上,model上就是根据条件去删除集合内某些文档,文档上就是删除自己。
model.remove(conditions, [callback])
document.remove([callback])
示例:
//model删除animalModel.remove({title :{$eq: '喵星人'}},(err,rs) =>{if (err) {console.info("删除失败");console.info(err);} else {console.info("删除成功");console.info(rs);}});//document删除animalModel.where('title').eq('2号猴猴').exec((err, rs) => {if (err) {console.info(err);} else {console.info(rs);rs.forEach(function(item,index,array){item.remove((err,rs) =>{if (err) {console.info("删除失败");console.info(err);} else {console.info("删除成功");console.info(rs);}});});}});
- 2、findOneAndRemove()
model的remove()会删除符合条件的所有数据,如果只删除符合条件的第一条数据,可以使用model的findOneAndRemove()方法
Model.findOneAndRemove(conditions, [options], [callback])
- 3、findByIdAndRemove()
通过id删除文档
Model.findByIdAndRemove(id, [options], [callback])
- 4、其他
所有删除方法的回调函数都不能省略,否则不成功。要么写在方法的回调函数参数中,如果不写回调函数参数,则可以在最后调用.exec()。
8、自定义方法
mongoose中可以利用已有的api,自定义自己的方法来处理特定的业务需要。
- 1、实例方法
通过new model得到的document实例有很多增删改查的方法,如上面的save()方法用来保存文档,我们可以给实例增加自定义方法来实现特定的业务逻辑,增加之后就可以像调用save一样来调用自己的方法,给document增加自定义方法是通过扩展Schema的methods属性来完成的。
为animalSchema扩展document实例方法:
//为document实例添加eat方法,返回所有喜欢吃的食物
animalSchema.methods.eat = function () {let foodList = this.food;let foodStr = '';foodList.forEach(function (item, index, arr) {if (index == 0) {foodStr += item.name;} else {foodStr += ","+item.name;}});return foodStr;
};
document实例调用:
animalModel.findById('5a96607b5ce650532494f011').exec((err, rs) => {if (err) {console.info("查询失败");console.info(err);} else {console.info("查询成功");console.info(rs);let eatStr = rs.eat();console.info(eatStr); //骨头,肉肉}});
- 2、静态方法
静态方法,是不需要实例化得到document,而是直接在model上调用的方法。
所以,实例方法,是操作文档(mongodb中的某一条记录)数据用的,静态方法是操作集合用的。
为animalSchema添加静态方法:
//为model添加findDogs方法
animalSchema.statics.findDogs = function (backfun){this.find({title: /狗/}).exec(backfun);
};
model调用:
animalModel.findDogs((err, rs) => {if (err) {console.info("查询失败");console.info(err);} else {console.info("查询成功");console.info(rs); //打印出两条狗的document}});
-3、查询方法
扩展schema对象的query属性,给model添加查询方法,主要为了扩展query的个性化方法需要。
//为query添加findMonkey方法
animalSchema.query.findMonkey = function (backfun){this.find({title: /猴/}).exec(backfun);
};
query调用:
let rsQuery = animalModel.find();rsQuery.findMonkey((err, rs) => {if (err) {console.info("查询失败");console.info(err);} else {console.info("查询成功");console.info(rs); //打印出一只猴子的document}});
9、虚拟属性
schema上除了可以设置方法以外,还可以设置虚拟属性,就像vue的getter一样,实际数据库中没有这个属性,但可以通过虚拟属性机制自定义一个经过处理的值,虽然用方法同样能实现,但虚拟属性效率更高。
//添加虚拟属性nameAndSex
animalSchema.virtual('info').get(function () {return this.title + "," +this.sex;
});
调用:
animalModel.find({title: /狗/}).exec((err, rs) => {if (err) {console.info("查询失败");console.info(err);} else {console.info("查询成功");rs.forEach( function (item, index, arr) {console.info(item.info);});}});
打印结果:
1号狗狗,male
2号狗狗,male
10、前置与后置钩子
通过schema可以为指定的某些方法添加前置钩子函数与后置钩子函数。前置钩子函数在指定方法开始前调用,后置钩子函数不是在指定方法结束后操作,二是在指定方法开始前的最后一步操作。
前置钩子是pre()
后置钩子是post()
//为find方法指定前置操作1
animalSchema.pre('find', function (next) {console.info('前置操作1');next();
});//为find方法指定前置操作2
animalSchema.pre('find', function (next) {console.info('前置操作2');next();
});//为find方法指定后置操作1
animalSchema.post('find', function () {console.info('后置操作1');
});//为find方法指定后置操作2
animalSchema.post('find', function () {console.info('后置操作2');
});
调用find()方法
animalModel.find({title: /狗/}).exec((err, rs) => {if (err) {console.info("查询失败");console.info(err);} else {console.info("查询成功");console.info(rs);}});
得到打印结果:
前置操作1
前置操作2
后置操作1
后置操作2
查询成功
[ { food: [ [Object], [Object] ],_id: 5a96607b5ce650532494f011,title: '1号狗狗',sex: 'male',__v: 0 },{ food: [ [Object], [Object] ],_id: 5a96607b5ce650532494f014,title: '2号狗狗',sex: 'male',__v: 0 } ]
注意,前置钩子中有一个next函数,如果方法内不调用next(),被设置前置钩子的方法(比如find())执行到钩子时不会继续向下执行。
可以添加前后钩子的方法有:
init
validate
save
remove
count
find
findOne
findOneAndRemove
findOneAndUpdate
insertMany
update
以上是mongoose的常用基本操作,还有索引、聚合等等操作需要积累补充。