0%

Mongoose

Mongoose 用于操作mongoDB的工具,mongodb提供的原驱动也可以,但是较难操作,故使用mongoose

Mongoose 入门参考

转载说明

源于Mongoose学习参考文档——基础篇
源于Node.js 手册查询-3-Mongoose 方法

名词解释

  • Schema : 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力
  • Model : 由Schema发布生成的模型,具有抽象属性和行为的数据库操作对
  • Entity : 由Model创建的实体,他的操作也会影响数据库

约定

  1. 本学习文档采用严格命名方式来区别不同对象,例如:

    • var PersonSchema; //Person的文本属性
    • var PersonModel; //Person的数据库模型
    • var PersonEntity; //Person实体
  2. Schema、Model、Entity的关系请牢记,Schema生成Model,Model创造Entity,Model和Entity都可对数据库操作造成影响,但Model比Entity更具操作性。

Schema

一种以文件形式存储的数据库模型骨架,无法直接通往数据库端,也就是说它不具备对数据库的操作能力.可以说是数据属性模型(传统意义的表结构),又或着是“集合”的模型骨架

1
2
3
4
5
6
7
8
9
/* 定义一个 Schema */
var mongoose = require("mongoose");

var TestSchema = new mongoose.Schema({
name : { type:String },//属性name,类型为String
age : { type:Number, default:0 },//属性age,类型为Number,默认为0
time : { type:Date, default:Date.now },
email: { type:String,default:''}
});

上面这个 TestSchema包含4个属性 [name, age, time, email]

Model

由Schema构造生成的模型,除了Schema定义的数据库骨架以外,还具有数据库操作的行为,类似于管理数据库属性、行为的类

1
2
3
4
5
var db = mongoose.connect("mongodb://127.0.0.1:27017/test");

// 创建Model
var TestModel = db.model("test1", TestSchema);
test1 数据库中的集合名称, 不存在会创建.

Entity

由Model创建的实体,使用save方法保存数据,Model和Entity都有能影响数据库的操作,但Model比Entity更具操作性

1
2
3
4
5
6
7
var TestEntity = new TestModel({
name : "Lenka",
age : 36,
email: "lenka@qq.com"
});
console.log(TestEntity.name); // Lenka
console.log(TestEntity.age); // 36

可以使用的数据类型

数据库存储的各种数据是需要预先设置其数据类型的,并且需要在Scheme中设置,我们称这些数据类型为Scheme.Type

Schema.Type是由Mongoose内定的一些数据类型,基本数据类型都在其中,他也内置了一些Mongoose特有的Schema.Type。当然,你也可以自定义Schema.Type,只有满足Schema.Type的类型才能定义在Schema内。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ExampleSchema = new Schema({
name:String,
binary:Buffer,
living:Boolean,
updated:Date,
age:Number,
mixed:Schema.Types.Mixed, //该混合类型等同于nested
_id:Schema.Types.ObjectId, //主键
_fk:Schema.Types.ObjectId, //外键
array:[],
arrOfString:[String],
arrOfNumber:[Number],
arrOfDate:[Date],
arrOfBuffer:[Buffer],
arrOfBoolean:[Boolean],
arrOfMixed:[Schema.Types.Mixed],
arrOfObjectId:[Schema.Types.ObjectId]
nested:{
stuff:String,
}
});

关于Buffer

Buffer和ArrayBuffer是Nodejs两种隐藏的对象,相关内容请查看NodeJS-API

关于Mixed

Schema.Types.Mixed是Mongoose定义个混合类型,该混合类型如果未定义具体形式。因此,如果定义具体内容,就直接使用{}来定义,以下两句等价

var AnySchema = new Schema({any:{}});
var AnySchema = new Schema({any:Schema.Types.Mixed});

混合类型因为没有特定约束,因此可以任意修改,一旦修改了原型,则必须调用markModified()

person.anything = {x:[3,4,{y:'change'}]}
person.markModified('anything');//传入anything,表示该属性类型发生变化
person.save();

关于ObjectId

存储在mongodb集合中的每个文档(document)都有一个默认的主键_id,这个主键名称是固定的,它可以是mongodb支持的任何数据类型,默认是ObjectId。除非自己定义,方可覆盖

ObjectId是一个12字节的 BSON 类型字符串。按照字节顺序,依次代表:

  • 4字节:UNIX时间戳
  • 3字节:表示运行MongoDB的机器
  • 2字节:表示生成此_id的进程
  • 3字节:由一个随机数开始的计数器生成的值
    1
    2
    3
    4
    var mongoose = require('mongoose');
    var ObjectId = mongoose.Schema.Types.ObjectId;
    var StudentSchema = new Schema({}); //默认会有_id:ObjectId
    var TeacherSchema = new Schema({id:ObjectId});//只有id:ObjectId
    该类型的值由系统自己生成,从某种意义上几乎不会重复,生成过程比较复杂,有兴趣的朋友可以查看源码。

关于Array

Array在JavaScript编程语言中并不是数组,而是集合,因此里面可以存入不同的值,以下代码等价:

var ExampleSchema1 = new Schema({array:[]});
var ExampleSchema2 = new Schema({array:Array});
var ExampleSchema3 = new Schema({array:[Schema.Types.Mixed]});
var ExampleSchema4 = new Schema({array:[{}]});

附言

Schema不仅定义了文档结构和使用性能,还可以有扩展插件、实例方法、静态方法、复合索引、文档生命周期钩子

Schema可以定义插件,并且插件具有良好的可拔插性,请有兴趣的读者继续往后阅读或者查阅官方资料。

API

连接接数据库

1
2
var mongoose = require("mongoose");
var db = mongoose.connect("mongodb://localhost:27017/test");

db - 数据库操作

挂接数据库连接事件,参数1: 也可以是error.

1
db.connection.on(‘open’, callback);

Schema - 表结构

1.构造函数

1
new mongoose.Schema( { name:{type:String}, age:{type:Number, default:10}  } )

2.添加属性

1
Schema.add( { name: ‘String’, email: ‘String’, age: ‘Number’ } )

3.有时候Schema不仅要为后面的Model和Entity提供公共的属性,还要提供公共的方法

1
2
 Schema.method( ‘say’, function(){console.log(‘hello’);} )
//这样Model和Entity的实例就能使用这个方法了

4.添加静态方法

1
2
 Schema.static( ‘say’, function(){console.log(‘hello’);} )
//静态方法,只限于在Model层就能使用

5.追加方法

1
2
 Schema.methods.say = function(){console.log(‘hello’);};
//静态方法,只限于在Model层就能使用

model - 文档操作

1.构造函数, 参数1:集合名称, 参数2:Schema实例

1
db.model(“test1”, TestSchema );

2.查询, 参数1忽略,或为空对象则返回所有集合文档

1
model.find({}, callback);
1
2
model.find({},field,callback);
//过滤查询,参数2: {‘name’:1, ‘age’:0} 查询文档的返回结果包含name , 不包含age.(_id默认是1)
1
2
model.find({},null,{limit:20});
//过滤查询,参数3: 游标操作 limit限制返回结果数量为20个,如不足20个则返回所有.
1
2
model.findOne({}, callback);
//查询找到的第一个文档
1
2
model.findById(‘obj._id’, callback);
//查询找到的第一个文档,同上. 但是只接受 __id 的值查询

3.创建, 在集合中创建一个文档

1
Model.create(文档数据, callback))

4.更新,参数1:查询条件, 参数2:更新对象,可以使用MondoDB的更新修改器

1
Model.update(conditions, update, function(error)

5.删除, 参数1:查询条件

1
Model.remove(conditions,callback);

Entity - 文档操作

1.构造函数, 其实就是model的实例

1
new TestModel( { name:‘xueyou’, age:21 } );

2.创建, 在集合中创建一个文档.

1
Entity.save(callback);

CRUD进一步说明

查询

通常有2种查询方式,一种是直接查询,一种是链式查询(2种查询都是自己命名的)

直接查询

在查询时带有回调函数的,称之为直接查询,查询的条件往往通过API来设定,例如:

PersonModel.findOne({'name.last':'dragon'},'some select',function(err,person){
  //如果err==null,则person就能取到数据
});

具体的查询参数,请查询API

链式查询

在查询时候,不带回调,而查询条件通过API函数来制定,例如:

var query = PersonModel.findOne({'name.last':'dragon'});
query.select('some select');
query.exec(function(err,pserson){
//如果err==null,则person就能取到数据

});
这种方式相对直接查询,分的比较明细,如果不带callback,则返回query,query没有执行的预编译查询语句,该query对象执行的方法都将返回自己,只有在执行exec方法时才执行查询,而且必须有回调。

因为query的操作始终返回自身,我们可以采用更形象的链式写法

Person
  .find({ occupation: /host/ })
  .where('name.last').equals('Ghost')
  .where('age').gt(17).lt(66)
  .where('likes').in(['vaporizing', 'talking'])
  .limit(10)
  .sort('-occupation')
  .select('name occupation')
  .exec(callback);

更新

有许多方式来更新文件,以下是常用的传统方式:

1
2
3
4
PersonModel.findById(id,function(err,person){
person.name = 'MDragon';
person.save(function(err){});
});

这里,利用Model模型查询到了person对象,该对象属于Entity,可以有save操作,如果使用Model`操作,需注意:

1
2
3
4
5
6
7
PersonModel.findById(id,function(err,person){
person.name = 'MDragon';
var _id = person._id; //需要取出主键_id
delete person._id; //再将其删除
PersonModel.update({_id:_id},person,function(err){});
//此时才能用Model操作,否则报错
});

update第一个参数是查询条件,第二个参数是更新的对象,但不能更新主键,这就是为什么要删除主键的原因。

当然这样的更新很麻烦,可以使用$set属性来配置,这样也不用先查询,如果更新的数据比较少,可用性还是很好的:

PersonModel.update({_id:_id},{$set:{name:'MDragon'}},function(err){});

需要注意,Document的CRUD操作都是异步执行,callback第一个参数必须是err,而第二个参数各个方法不一样,update的callback第二个参数是更新的数量,如果要返回更新后的对象,则要使用如下方法

Person.findByIdAndUpdate(_id,{$set:{name:'MDragon'}},function(err,person){
  console.log(person.name); //MDragon
});

类似的方法还有findByIdAndRemove,如同名字,只能根据id查询并作update/remove操作,操作的数据仅一条

新增

如果是Entity,使用save方法,如果是Model,使用create方法

//使用Entity来增加一条数据
var krouky = new PersonModel({name:'krouky'});
krouky.save(callback);
//使用Model来增加一条数据
var MDragon = {name:'MDragon'};
PersonModel.create(MDragon,callback);

两种新增方法区别在于,如果使用Model新增时,传入的对象只能是纯净的JSON对象,不能是由Model创建的实体,原因是:由Model创建的实体krouky虽然打印是只有{name:'krouky'},但是krouky属于Entity,包含有Schema属性和Model数据库行为模型。如果是使用Model创建的对象,传入时一定会将隐藏属性也存入数据库,虽然3.x追加了默认严格属性,但也不必要增加操作的报错

删除

和新增一样,删除也有2种方式,但Entity和Model都使用remove方法

Sub Docs

如同SQL数据库中2张表有主外关系,Mongoose将2个Document的嵌套叫做Sub-Docs(子文档)

简单的说就是一个Document嵌套另外一个Document或者Documents:

var ChildSchema1 = new Schema({name:String});
var ChildSchema2 = new Schema({name:String});
var ParentSchema = new Schema({
  children1:ChildSchema1,   //嵌套Document
  children2:[ChildSchema2]  //嵌套Documents
});

Sub-Docs享受和Documents一样的操作,但是Sub-Docs的操作都由父类去执行

var ParentModel = db.model('Parent',parentSchema);
var parent = new ParentModel({
  children2:[{name:'c1'},{name:'c2'}]
});
parent.children2[0].name = 'd';
parent.save(callback);

parent在执行保存时,由于包含children2,他是一个数据库模型对象,因此会先保存chilren2[0]和chilren2[1]。

如果子文档在更新时出现错误,将直接报在父类文档中,可以这样处理:

ChildrenSchema.pre('save',function(next){
  if('x' === this.name) return next(new Error('#err:not-x'));
  next();
});
var parent = new ParentModel({children1:{name:'not-x'}});
parent.save(function(err){
  console.log(err.message); //#err:not-x
});

查询子文档

如果children是parent的子文档,可以通过如下方法查询到children

var child = parent.children.id(id);

新增、删除、更新

子文档是父文档的一个属性,因此按照属性的操作即可,不同的是在新增父类的时候,子文档是会被先加入进去的。

如果ChildrenSchema是临时的一个子文档,不作为数据库映射集合,可以这样:

var ParentSchema = new Schema({
  children:{
    name:String
  }
});
//其实就是匿名混合模式

修改器和更新器

更新修改器:

  • ‘$inc’ 增减修改器,只对数字有效.下面的实例: 找到 age=22的文档,修改文档的age值自增1

    1
    2
    Model.update({‘age’:22}, {’$inc’:{‘age’:1} }  );
    //执行后: age=23
  • ‘$set’ 指定一个键的值,这个键不存在就创建它.可以是任何MondoDB支持的类型.

    1
    2
    Model.update({‘age’:22}, {’$set’:{‘age’:‘haha’} }  );
    //执行后: age=‘haha’
  • ‘$unset’ 同上取反,删除一个键

    1
    2
    Model.update({‘age’:22}, {’$unset’:{‘age’:‘haha’} }  );
    //执行后: age键不存在

    数组修改器:

  • ‘$push’ 给一个键push一个数组成员,键不存在会创建

    1
    2
    Model.update({‘age’:22}, {’$push’:{‘array’:10} }  );
    //执行后: 增加一个 array 键,类型为数组, 有一个成员 10
  • ‘$addToSet’ 向数组中添加一个元素,如果存在就不添加

    1
    2
    Model.update({‘age’:22}, {’$addToSet’:{‘array’:10} }  );
    //执行后: array中有10所以不会添加
  • ‘$each’ 遍历数组, 和 $push 修改器配合可以插入多个值

    1
    2
    Model.update({‘age’:22}, {’$push’:{‘array’:{’$each’: [1,2,3,4,5]}} }  );
    //执行后: array : [10,1,2,3,4,5]
  • ‘$pop’ 向数组中尾部删除一个元素

    1
    2
    Model.update({‘age’:22}, {’$pop’:{‘array’:1} }  );
    //执行后: array : [10,1,2,3,4] tips: 将1改成-1可以删除数组首部元素
  • ‘$pull’ 向数组中删除指定元素

    1
    2
    Model.update({‘age’:22}, {’$pull’:{‘array’:10} }  );
    //执行后: array : [1,2,3,4] 匹配到array中的10后将其删除

    条件查询:

“$lt” 小于
“$lte” 小于等于
“$gt” 大于
“$gte” 大于等于
“$ne” 不等于
Model.find({“age”:{ “$get”:18 , “$lte”:30 } } );
查询 age 大于等于18并小于等于30的文档

或查询 OR:

‘$in’ 一个键对应多个值
‘$nin’ 同上取反, 一个键不对应指定值
“$or” 多个条件匹配, 可以嵌套 $in 使用
“$not” 同上取反, 查询与特定模式不匹配的文档

1
2
Model.find({“age”:{ “$in”:[20,21,22.‘haha’]} } );
//查询 age等于20或21或21或’haha’的文档
1
2
Model.find({"$or" :  [ {‘age’:18} , {‘name’:‘xueyou’} ] });
//查询 age等于18 或 name等于’xueyou’ 的文档

类型查询:

null 能匹配自身和不存在的值, 想要匹配键的值 为null, 就要通过 “$exists” 条件判定键值已经存在
“$exists” (表示是否存在的意思)

1
2
Model.find(“age” :  { “$in” : [null] , “exists” : true  } );
//查询 age值为null的文档
1
2
3
Model.find({name: {$exists: true}},function(error,docs){
//查询所有存在name属性的文档
});
1
2
3
Model.find({telephone: {$exists: false}},function(error,docs){
//查询所有不存在telephone属性的文档
});

正则表达式:

MongoDb 使用 Prel兼容的正则表达式库来匹配正则表达式

1
2
find( {“name” : /joe/i } )	
//查询name为 joe 的文档, 并忽略大小写
1
2
find( {“name” : /joe?/i } )
//查询匹配各种大小写组合

查询数组:

1
2
Model.find({“array”:10} );
//查询 array(数组类型)键中有10的文档, array : [1,2,3,4,5,10] 会匹配到
1
2
Model.find({“array[5]”:10} );
//查询 array(数组类型)键中下标5对应的值是10, array : [1,2,3,4,5,10] 会匹配到
  • ‘$all’ 匹配数组中多个元素
    1
    2
    Model.find({“array”:[5,10]} );
    //查询 匹配array数组中 既有5又有10的文档
  • ‘$size’ 匹配数组长度
    1
    2
    Model.find({“array”:{"$size" : 3} } );
    //查询 匹配array数组长度为3 的文档
  • ‘$slice’ 查询子集合返回
    1
    2
    Model.find({“array”:{"$skice" : 10} } );
    //查询 匹配array数组的前10个元素
    1
    2
    Model.find({“array”:{"$skice" : [5,10] } } );
    //查询 匹配array数组的第5个到第10个元素

    where

用它可以执行任意javacript语句作为查询的一部分,如果回调函数返回 true 文档就作为结果的一部分返回

1
2
3
4
5
6
7
8
9
10
11
12
13
	find( {"$where" : function(){
for( var x in this ){
//这个函数中的 this 就是文档
}

if(this.x !== null && this.y !== null){
return this.x + this.y === 10 ? true : false;
}else{
return true;
}


} } )

简化版本

1
2
find( {"$where" :  "this.x + this.y === 10" } )
find( {"$where" : " function(){ return this.x + this.y ===10; } " } )

游标

MongoDB 使用游标返回find的执行结果.客户端对游标的实现通常能够对最终结果进行有效的控制。可以限制结果的数量,略过部分结果,根据任意键按任意顺序的组合对结果进行各种排序,或者是执行其他一些强的操作。

  • limit(3) 限制返回结果的数量,
  • skip(3) 跳过前3个文档,返回其余的
  • sort( {“username”:1 , “age”:-1 } ) 排序 键对应文档的键名, 值代表排序方向, 1 升序, -1降序