Mongoose学习
Mongoose是位于MongoDB与NodeJS之间的,做了很多抽象,大大简化了使用MongoDB的难度。
Schema | Model | Entity
Mongoose 里,一切都始于Schema。
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
}
},
}
复制代码
更新
有三种方式来更新数据:
- 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: 如果为真,将原始结果作为返回
复制代码
例子:
// 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);
复制代码