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
}
},
}
複製程式碼
更新
有三種方式來更新資料:
- 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);
複製程式碼