Mongoose初步学习

4,100 阅读13分钟

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 两种隐藏的对象,相关内容请查看 NodeJS-API

1.1---ObjectId

用Schema.Types.ObjectId 来声明一个对象ID类型。对象ID同MongoDB内置的_id 的类型,是一个24位Hash字符串。

const mongoose = require('mongoose')
const ObjectId = mongoose.Schema.Types.ObjectId
const 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)

// or

const 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)  // open
const 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 Oinstance
try {
    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
        }
    },
}

更新

有三种方式来更新数据:

  1. update 该方法会匹配到所查找的内容进行更新,不会返回数据
  2. updateone 一次更新一条
  3. updateMany 一次更新多条
  4. findOneAndUpdate 该方法会根据查找去更新数据库,另外也会返回查找到的并未改变的数据
  5. 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: 如果为真,将原始结果作为返回

例子:

// START
async 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)
})

例子:

// START
async 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);