Mongoose:優雅地在NodeJS中進行MongoDB對象建模。html
咱們開發Mongoose是由於(開發者)寫MongoDB的驗證機制、類型轉換與業務邏輯模板很麻煩。node
針對爲應用數據建模的問題,Mongoose 提供了一套直白的,基於模式的解決方案。包括了內建的類型轉換、驗證器、查詢構造器、業務邏輯鉤子等。git
Mongoose的地位是位於MongoDB與NodeJS之間的,看上去是增長了一些複雜度,但實際上卻作了不少抽象,大大簡化了使用MongoDB的難度。github
咱們結合koa作項目展現,克隆下面項目地址mongodb
https://github.com/daly-young/mongoosebasic.git
複製代碼
運行:數據庫
node demos/index.js
複製代碼
Schema : 一種以文件形式存儲的數據庫模型骨架,不具有數據庫的操做能力api
Model : 由Schema發佈生成的模型,具備抽象屬性和行爲的數據庫操做對數組
Entity : 由Model建立的實體,他的操做也會影響數據庫安全
Schema、Model、Entity的關係請牢記,Schema生成Model,Model創造Entity,Model和Entity均可對數據庫操做形成影響,但Model比Entity更具操做性。app
schema是mongoose裏會用到的一種數據模式,能夠理解爲表結構的定義;每一個schema會映射到mongodb中的一個collection,它不具有操做數據庫的能力
在根目錄建models文件夾,咱們定義一個user的Schema,命名爲user.js
const UserSchema = new mongoose.Schema({
userName: String
})
複製代碼
定義一個Schema就這麼簡單,指定字段名和類型。
Schema.Type是由Mongoose內定的一些數據類型,基本數據類型都在其中,它也內置了一些Mongoose特有的Schema.Type。固然,你也能夠自定義Schema.Type,只有知足Schema.Type的類型才能定義在Schema內。
Schema Types內置類型以下: String, Number, Boolean | Bool, Array, Buffer, Date, ObjectId, Mixed
Buffer 類的實例相似於整數數組,但 Buffer 的大小是固定的、且在 V8 堆外分配物理內存。 Buffer 的大小在被建立時肯定,且沒法調整。 Buffer 類是一個全局變量類型,用來直接處理二進制數據的。 它可以使用多種方式構建。
Buffer 和 ArrayBuffer 是 Nodejs 兩種隱藏的對象,相關內容請查看 NodeJS-API
用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 })
複製代碼
混合型是一種「存啥都行」的數據類型,它的靈活性來自於對可維護性的妥協。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(); // 改變值被保存
複製代碼
數據的存儲是須要驗證的,不是什麼數據都能往數據庫裏丟或者顯示到客戶端的,數據的驗證須要記住如下規則:
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是驗證失敗的錯誤信息
}
});
複製代碼
若是驗證失敗,則會返回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屬性
在使用new Schema(config)時,咱們能夠追加一個參數options來配置Schema的配置,例如:
const ExampleSchema = new Schema(config,options)
// or
const ExampleSchema = new Schema(config)
ExampleSchema.set(option,value)
複製代碼
Options:
通常可作以下配置:
new Schema({...},{safe:true})
複製代碼
固然咱們也能夠這樣
new Schema({...},{safe:{j:1,w:2,wtimeout:10000}})
複製代碼
j表示作1份日誌,w表示作2個副本(尚不明確),超時時間10秒
默認是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,表示出現問題將會拋出錯誤
若是有數據庫的批量操做,該屬性能限制一次操做的量,例如:
new Schema({...},{capped:1024}) // can operate 1024 at most once
複製代碼
固然該參數也但是JSON對象,包含size、max、autiIndexId屬性
new Schema({...},{capped:{size:1024,max:100,autoIndexId:true}})
複製代碼
版本鎖是Mongoose默認配置(__v屬性)的,若是你想本身定製,以下:
new Schema({...},{versionKey:'__someElse'});
複製代碼
此時存入數據庫的版本鎖就不是__v屬性,而是__someElse,至關因而給版本鎖取名字。 具體怎麼存入都是由Mongoose和MongoDB本身決定,固然,這個屬性你也能夠去除。
new Schema({...},{versionKey:false});
複製代碼
除非你知道你在作什麼,而且你知道這樣作的後果
應用開始的時候,Mongoose對每個索引起送一個ensureIndex的命令。索引默認(_id)被Mongoose建立。
當咱們不須要設置索引的時候,就能夠經過設置這個選項。
const schema = new Schema({..}, { autoIndex: false }) const Clock = mongoose.model('Clock', schema) Clock.ensureIndexes(callback)
有的時候,咱們創造的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
}
複製代碼
靜態方法在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)
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模型,是通過Schema構造來的,除了Schema定義的數據庫骨架之外,還具備數據庫行爲模型,他至關於管理數據庫屬性、行爲的類。
實際上,Model纔是操做數據庫最直接的一塊內容. 咱們全部的CRUD就是圍繞着Model展開的。
你必須經過Schema來建立,以下:
const TankSchema = new Schema({
name:'String',
size:'String'
})
const TankModel = mongoose.model('Tank',TankSchema)
複製代碼
該模型就能直接拿來操做,具體查看API,例如:
const tank = {'something',size:'small'}
TankModel.create(tank)
複製代碼
注意:
你可使用Model來建立Entity,Entity實體是一個特有Model具體對象,可是他並不具有Model的方法,只能用本身的方法。
const tankEntity = new TankModel('someother','size:big');
tankEntity.save()
複製代碼
若是是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
}
},
}
複製代碼
有三種方式來更新數據:
三個方法都包含四個參數,稍微說明一下幾個參數的意思:
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方法有兩種使用方式,一種是用在模型上,另外一種是用在模型實例上,例如:
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
複製代碼
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,可使用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);
複製代碼