Mongoose初步學習

daly發表於2018-04-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);
複製程式碼

相關文章