Mongoose:优雅地在NodeJS中进行MongoDB对象建模。
我们开发Mongoose是因为(开发者)写MongoDB的验证机制、类型转换与业务逻辑模板很麻烦。
针对为应用数据建模的问题,Mongoose 提供了一套直白的,基于模式的解决方案。包括了内建的类型转换、验证器、查询构造器、业务逻辑钩子等。
Mongoose的地位是位于MongoDB与NodeJS之间的,看上去是增加了一些复杂度,但实际上却做了很多抽象,大大简化了使用MongoDB的难度。
项目安装
我们结合koa做项目展示,克隆下面项目地址
https://github.com/daly-young/mongoosebasic.git复制代码
运行:
node demos/index.js复制代码
Schema | Model | Entity
Schema : 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力
Model : 由Schema发布生成的模型,具有抽象属性和行为的数据库操作对
Entity : 由Model创建的实体,他的操作也会影响数据库
Schema、Model、Entity的关系请牢记,Schema生成Model,Model创造Entity,Model和Entity都可对数据库操作造成影响,但Model比Entity更具操作性。
Schema
schema是mongoose里会用到的一种数据模式,可以理解为表结构的定义;每个schema会映射到mongodb中的一个collection,它不具备操作数据库的能力
在根目录建models文件夹,我们定义一个user的Schema,命名为user.js
const UserSchema = new mongoose.Schema({ userName: String})复制代码
定义一个Schema就这么简单,指定字段名和类型。
1---Schema.Type
Schema.Type是由Mongoose内定的一些数据类型,基本数据类型都在其中,它也内置了一些Mongoose特有的Schema.Type。当然,你也可以自定义Schema.Type,只有满足Schema.Type的类型才能定义在Schema内。
Schema Types内置类型如下: String, Number, Boolean | Bool, Array, Buffer, Date, ObjectId, Mixed
1.0---Buffer
Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在被创建时确定,且无法调整。 Buffer 类是一个全局变量类型,用来直接处理二进制数据的。 它能够使用多种方式构建。
Buffer 和 ArrayBuffer 是 Nodejs 两种隐藏的对象,
1.1---ObjectId
用Schema.Types.ObjectId 来声明一个对象ID类型。对象ID同MongoDB内置的_id 的类型,是一个24位Hash字符串。
const mongoose = require('mongoose')const ObjectId = mongoose.Schema.Types.ObjectIdconst Car = new Schema({ driver: ObjectId })复制代码
1.2---Mixed
混合型是一种“存啥都行”的数据类型,它的灵活性来自于对可维护性的妥协。Mixed类型用Schema.Types.Mixed 或者一个字面上的空对象{}来定义。下面的定义是等价的:
const AnySchema = new Schema({any:{}})const AnySchema = new Schema({any:Schema.Types.Mixed})复制代码
混合类型因为没有特定约束,因此可以任意修改,一旦修改了原型,则必须调用markModified()
person.anything = {x:[3,4,{y:'change'}]}person.markModified('anything') // 输入值,意味着这个值要改变person.save(); // 改变值被保存复制代码
2---Validation
数据的存储是需要验证的,不是什么数据都能往数据库里丢或者显示到客户端的,数据的验证需要记住以下规则:
- 验证始终定义在SchemaType中
- 验证是一个内部中间件
- 验证是在一个Document被保存时默认启用的,除非你关闭验证
- 验证是异步递归的,如果你的SubDoc验证失败,Document也将无法保存
- 验证并不关心错误类型,而通过ValidationError这个对象可以访问
2.1---验证器 ####=
required 非空验证 min/max 范围验证(边值验证) enum/match 枚举验证/匹配验证 validate 自定义验证规则
以下是综合案例:
var PersonSchema = new Schema({ name:{ type:'String', required:true //姓名非空 }, age:{ type:'Nunmer', min:18, //年龄最小18 max:120 //年龄最大120 }, city:{ type:'String', enum:['北京','上海'] //只能是北京、上海人 }, other:{ type:'String', validate:[validator,err] //validator是一个验证函数,err是验证失败的错误信息 }});复制代码
2.2---验证失败
如果验证失败,则会返回err信息,err是一个对象该对象属性如下
err.errors //错误集合(对象)err.errors.color //错误属性(Schema的color属性)err.errors.color.message //错误属性信息err.errors.path //错误属性路径err.errors.type //错误类型err.name //错误名称err.message //错误消息复制代码
一旦验证失败,Model和Entity都将具有和err一样的errors属性
3---配置项
在使用new Schema(config)时,我们可以追加一个参数options来配置Schema的配置,例如:
const ExampleSchema = new Schema(config,options)// orconst ExampleSchema = new Schema(config)ExampleSchema.set(option,value)复制代码
Options:
- autoIndex: bool - defaults to null (which means use the connection's autoIndex option)
- bufferCommands: bool - defaults to true
- capped: bool - defaults to false
- collection: string no default
- id: bool defaults to true
- _id: bool defaults to true
- minimize: bool controls document#toObject behavior when called manually defaults to true
- read: string
- safe: bool defaults to true.
- shardKey: bool defaults to null
- strict: bool defaults to true
- toJSON object no default
- toObject object no default
- typeKey string defaults to 'type'
- useNestedStrict boolean defaults to false
- validateBeforeSave bool defaults to true
- versionKey: string defaults to "__v"
- collation: object defaults to null (which means use no collation)
3.1---safe——安全属性(默认安全)
一般可做如下配置:
new Schema({...},{safe:true})复制代码
当然我们也可以这样
new Schema({...},{safe:{j:1,w:2,wtimeout:10000}})复制代码
j表示做1份日志,w表示做2个副本(尚不明确),超时时间10秒
3.2---strict——严格配置(默认启用)
默认是enabled,确保Entity的值存入数据库前会被自动验证,如果实例中的域(field)在schema中不存在,那么这个域不会被插入到数据库。 如果你没有充足的理由,请不要停用,例子:
const ThingSchema = new Schema({a:String})const ThingModel = db.model('Thing',SchemaSchema)const thing = new ThingModel({iAmNotInTheThingSchema:true})thing.save() // iAmNotInTheThingSchema will not be saved复制代码
如果取消严格选项,iAmNotInTheThingSchema将会被存入数据库
该选项也可以在构造实例时使用,例如:
const ThingModel = db.model('Thing')const thing1 = new ThingModel(doc,true) // openconst thing2 = new ThingModel(doc,false) // close复制代码
注意:strict也可以设置为throw,表示出现问题将会抛出错误
3.3---capped——上限设置
如果有数据库的批量操作,该属性能限制一次操作的量,例如:
new Schema({...},{capped:1024}) // can operate 1024 at most once复制代码
当然该参数也可是JSON对象,包含size、max、autiIndexId属性
new Schema({...},{capped:{size:1024,max:100,autoIndexId:true}})复制代码
3.4---versionKey——版本锁
版本锁是Mongoose默认配置(__v属性)的,如果你想自己定制,如下:
new Schema({...},{versionKey:'__someElse'});复制代码
此时存入数据库的版本锁就不是__v属性,而是__someElse,相当于是给版本锁取名字。 具体怎么存入都是由Mongoose和MongoDB自己决定,当然,这个属性你也可以去除。
new Schema({...},{versionKey:false});复制代码
除非你知道你在做什么,并且你知道这样做的后果
3.5--- autoIndex——自动索引
应用开始的时候,Mongoose对每一个索引发送一个ensureIndex的命令。索引默认(_id)被Mongoose创建。
当我们不需要设置索引的时候,就可以通过设置这个选项。
const schema = new Schema({..}, { autoIndex: false }) const Clock = mongoose.model('Clock', schema) Clock.ensureIndexes(callback)
4---Schema的扩展
4.1 实例方法
有的时候,我们创造的Schema不仅要为后面的Model和Entity提供公共的属性,还要提供公共的方法。
下面例子比快速通道的例子更加高级,可以进行高级扩展:
const schema = new Schema({ name: String, type: String})// 检查相似数据schema.methods.findSimilarTypes = () => { return mongoose.model('Oinstance').find({ type: 'engineer' })}const Oinstance = mongoose.model('Oinstance', schema)module.exports = Oinstance复制代码
使用如下:
const Oinstance = require('../models/06instance-method')const m = new Oinstancetry { let res = await m.findSimilarTypes() ctx.body = res} catch (e) { console.log('!err==', e) return next}复制代码
4.2 静态方法
静态方法在Model层就能使用,如下:
const schema = new Schema({ name: String, type: String})schema.statics.findSimilarTypes = () => { return mongoose.model('Ostatic').find({ type: 'engineer' })}// 例子const Ostatic = mongoose.model('Ostatic', schema)module.exports = Ostatic复制代码
使用如下: try { let res = await Ostatic.findSimilarTypes() ctx.body = res } catch (e) { console.log('!err==', e) return next }
methods和statics的区别
区别就是一个给Model添加方法(statics),一个给实例添加方法(methods)
4.3 虚拟属性
Schema中如果定义了虚拟属性,那么该属性将不写入数据库,例如:
const PersonSchema = new Schema({ name:{ first:String, last:String }})const PersonModel = mongoose.model('Person',PersonSchema)const daly = new PersonModel({ name:{first:'daly',last:'yang'}})复制代码
如果每次想使用全名就得这样
console.log(daly.name.first + ' ' + daly.name.last);复制代码
显然这是很麻烦的,我们可以定义虚拟属性:
PersonSchema.virtual('name.full').get(function(){ return this.name.first + ' ' + this.name.last;});复制代码
那么就能用daly.name.full来调用全名了,反之如果知道full,也可以反解first和last属性
PersonSchema.virtual('name.full').set(function(name){ var split = name.split(' '); this.name.first = split[0]; this.name.last = split[1];});var PersonModel = mongoose.model('Person',PersonSchema);var krouky = new PersonModel({});krouky.name.full = 'daly yang';console.log(krouky.name.first);复制代码
Model
1---什么是Model
Model模型,是经过Schema构造来的,除了Schema定义的数据库骨架以外,还具有数据库行为模型,他相当于管理数据库属性、行为的类。
实际上,Model才是操作数据库最直接的一块内容. 我们所有的CRUD就是围绕着Model展开的。
2---如何创建Model
你必须通过Schema来创建,如下:
const TankSchema = new Schema({ name:'String', size:'String' })const TankModel = mongoose.model('Tank',TankSchema)复制代码
3---操作Model
该模型就能直接拿来操作,具体查看API,例如:
const tank = {'something',size:'small'}TankModel.create(tank)复制代码
注意:
你可以使用Model来创建Entity,Entity实体是一个特有Model具体对象,但是他并不具备Model的方法,只能用自己的方法。
const tankEntity = new TankModel('someother','size:big');tankEntity.save()复制代码
实例
增加
- save()
- create()
- insertOne() 插入单条数据
- insertMany() 比create方法快,因为是多条数据一次操作
如果是Entity,使用save方法,如果是Model,使用create方法
module.exports = { async mCreateModal(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body try { // Modal创建 let data = await Ocrud.create(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!err==', e) result.code = -1 result.resultDes = e ctx.body = result return next } }, async mCreateEntity(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body const user = new Ocrud(param) try { // Entity创建 let data = await user.save() result.success = true result.data = data ctx.body = result } catch (e) { console.log('!err==', e) result.code = -2 result.resultDes = e ctx.body = result return next } }, async mInsertMany(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.users try { let data = await user.insertMany(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!err==', e) result.code = -2 result.resultDes = e ctx.body = result return next } },}复制代码
更新
有三种方式来更新数据:
- update 该方法会匹配到所查找的内容进行更新,不会返回数据
- updateone 一次更新一条
- updateMany 一次更新多条
- findOneAndUpdate 该方法会根据查找去更新数据库,另外也会返回查找到的并未改变的数据
- findByIdAndUpdate 该方法跟上面的findOneAndUpdate方法功能一样,不过他是根据ID来查找文档并更新的
三个方法都包含四个参数,稍微说明一下几个参数的意思:
Model.update(conditions, doc, [options], [callback])复制代码
conditions:查询条件
update:更新的数据对象,是一个包含键值对的对象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模式,允许覆盖记录复制代码
对于options参数,在update方法中和findOneAndUpdate、findByIdAndUpdate两个方法中的可选设置是不同的;
在update方法中,options的可选设置为:
{ safe:true|false, //声明是否返回错误信息,默认true upsert:false|true, //声明如果查询不到需要更新的数据项,是否需要新插入一条记录,默认false multi:false|true, //声明是否可以同时更新多条记录,默认false strict:true|false //声明更新的数据中是否可以包含在schema定义之外的字段数据,默认true}复制代码
findOneAndUpdate,options可选设置项为:
new: bool - 默认为false。返回修改后的数据。upsert: bool - 默认为false。如果不存在则创建记录。fields: {Object|String} - 选择字段。类似.select(fields).findOneAndUpdate()。maxTimeMS: 查询用时上限。sort: 如果有多个查询条件,按顺序进行查询更新。runValidators: 如果值为true,执行Validation验证。setDefaultsOnInsert: 如果upsert选项为true,在新建时插入文档定义的默认值。rawResult: 如果为真,将原始结果作为回调函数第三个参数。复制代码
findByIdAndUpdate,options可选设置项为:
new: bool - 默认为false。返回修改后的数据。upsert: bool - 默认为false。如果不存在则创建记录。runValidators: 如果值为true,执行Validation验证。setDefaultsOnInsert: 如果upsert选项为true,在新建时插入文档定义的默认值。sort: 如果有多个查询条件,按顺序进行查询更新。select: 设置返回的数据字段rawResult: 如果为真,将原始结果作为返回复制代码
例子:
// STARTasync mUpdate(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let condition = ctx.request.body.condition let doc = ctx.request.body.doc console.log(condition, '===condition') console.log(doc, '===doc') try { let data = await Ocrud.update(condition, doc, { multi: true }) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mUpdateOne(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let condition = ctx.request.body.condition let doc = ctx.request.body.doc try { let data = await Ocrud.updateOne(condition, doc) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mUpdateMany(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let condition = ctx.request.body.condition let doc = ctx.request.body.doc try { let data = await Ocrud.updateMany(condition, doc, { multi: true }) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mFindOneAndUpdate(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let condition = ctx.request.body.condition let doc = ctx.request.body.doc try { let data = await Ocrud.findOneAndUpdate(condition, doc, { new: true, rawResult: true }) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mFindByIdAndUpdate(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let _id = ctx.request.body.id let doc = ctx.request.body.doc try { let data = await Ocrud.findByIdAndUpdate(_id, doc) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},// END 复制代码
删除
- remove() 删除所有符合条件的文档,如果只想删除第一个符合条件的对象,可以添加设置single为true
- delete() 删除第一个符合对象的文档,会忽视single的值
- deleteMany() 删除所有符合条件的文档,会忽视single的值
- findOneAndRemove()
- findByIdAndRemove()
remove方法有两种使用方式,一种是用在模型上,另一种是用在模型实例上,例如:
User.remove({ name : /Simon/ } , function (err){ if (!err){ // 删除名字中包含simon的所有用户 }});User.findOne({ email : 'simon@theholmesoffice.com'},function (err,user){ if (!err){ user.remove( function(err){ // 删除匹配到该邮箱的第一个用户 }) }})复制代码
接下来看一下findOneAndRemove方法: sort: 如果有多个查询条件,按顺序进行查询更新 maxTimeMS: 查询用时上限 requires mongodb >= 2.6.0 select: 设置返回的数据字段 rawResult: 如果为真,将原始结果返回
User.findOneAndRemove({name : /Simon/},{sort : 'lastLogin', select : 'name email'},function (err, user){ if (!err) { console.log(user.name + " removed"); // Simon Holmes removed }})复制代码
另外一个findByIdAndRemove方法则是如出一辙的。 sort: 如果有多个查询条件,按顺序进行查询更新 select: 设置返回的数据字段 rawResult: 如果为真,将原始结果返回
User.findByIdAndRemove(req.body._id,function (err, user) { if(err){ console.log(err) return } console.log("User deleted:", user)})复制代码
例子:
// STARTasync mDelete(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body.condition try { let data = await Ocrud.delete(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mRemove(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body.condition try { let data = await Ocrud.remove(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mDeleteMany(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body.condition try { let data = await Ocrud.deleteMany(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mFindOneAndRemove(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body.condition try { let data = await Ocrud.findOneAndRemove(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},async mFindByIdAndRemove(ctx, next) { let result = { success: false, code: 0, resultDes: "" } let param = ctx.request.body.id try { let data = await Ocrud.findByIdAndRemove(param) result.success = true result.data = data ctx.body = result } catch (e) { console.log('!er==', e) result.code = -3 result.resultDes = e ctx.body = result return next }},// END 复制代码
综合写法
- bulkWrite() 可以一次发送insertOne, updateOne, updateMany, replaceOne, deleteOne, and/or deleteMany多种操作命令,比单条命令一次发送效率要高
Character.bulkWrite([ { insertOne: { document: { name: 'Eddard Stark', title: 'Warden of the North' } } }, { updateOne: { filter: { name: 'Eddard Stark' }, // If you were using the MongoDB driver directly, you'd need to do // `update: { $set: { title: ... } }` but mongoose adds $set for // you. update: { title: 'Hand of the King' } } }, { deleteOne: { { filter: { name: 'Eddard Stark' } } } }]).then(handleResult)复制代码
Query
Query构造函数被用来构建查询,不需直接实例化Query,可以使用MOdel函数像 MOdel.find()
const query = MyModel.find(); // `query` is an instance of `Query`query.setOptions({ lean : true });query.collection(model.collection);query.where('age').gte(21).exec(callback);// You can instantiate a query directly. There is no need to do// this unless you're an advanced user with a very good reason to.const query = new mongoose.Query();复制代码
链式查询
因为query的操作始终返回自身,我们可以采用更形象的链式写法
query .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);复制代码