1. 程式人生 > >深入淺出mongoose【轉】

深入淺出mongoose【轉】

mongoose是nodeJS提供連線 mongodb的一個庫. 此外還有mongoskin, mongodb(mongodb官方出品). 本人,還是比較青睞mongoose的, 因為他遵循的是一種, 模板式方法, 能夠對你輸入的資料進行自動處理. 有興趣的同學可以去Mongoose官網看看.

初入mongoose

install mongoose

I’ve said that. 使用mongoose你需要有 nodeJS和mongodb資料庫. 這兩個東西, 前端寶寶們應該很清楚了》 下載mongoose:

npm install mongoose --save

connect mongoose

下載好資料庫之後,我們 來碰碰運氣, 看你能不能連線上database. 首先,開啟你的mongodb;

mongod;  //這裡我已經將mongodb放在環境變數中了

資料庫成功開啟後: 在js檔案中寫入:

'use strict';

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/test');
const con = mongoose.connection;
con.on('error', console.error.bind(console, '連線資料庫失敗'));
con.once('open',()=>{
    //成功連線
})

(其實,懂得童鞋,你只要copy就行了.) OK,運氣好的同學,此刻嘴角揚起45°的微笑. 運氣差的同學, 出門左轉google. 簡單連線mongoose後,我們來看一看mongoose的基本構造吧.

understand mongoose

mongoose實際上,可以說是Oracle和mongodb的一個混合產物,但歸根接地還是mongodb的. 這裡我要祭出,我珍藏很久的對比圖. 熟悉其他資料庫的同學應該能很快明白的.

Oracle MongoDB Mongoose
資料庫例項(database instance) MongoDB例項 Mongoose
模式(schema) 資料庫(database) mongoose
表(table) 集合(collection) 模板(Schema)+模型(Model)
行(row) 文件(document) 例項(instance)
rowid _id _id
Join DBRef DBRef

通過上面的闡述,我們大概能知道了在Mongoose裡面有哪幾個基本概念.

  • Schema: 相當於一個數據庫的模板. Model可以通過mongoose.model 整合其基本屬性內容. 當然也可以選擇不繼承.
  • Model: 基本文件資料的父類,通過整合Schema定義的基本方法和屬性得到相關的內容.
  • instance: 這就是實實在在的資料了. 通過 new Model()初始化得到.

他們各自間是怎樣的關係呢? 下圖可以清晰的說明, 以上3中實際上就是一個繼承一個得到最後的資料.

mongoose

我們先看一個demo吧:

'use strict';

const mongoose = require('mongoose');


mongoose.connect('mongodb://localhost:27017/test');
const con = mongoose.connection;
con.on('error', console.error.bind(console, '連線資料庫失敗'));
con.once('open',()=>{
    //定義一個schema
    let Schema = mongoose.Schema({
        category:String,
        name:String
    });
    Schema.methods.eat = function(){
        console.log("I've eatten one "+this.name);
    }
    //繼承一個schema
    let Model = mongoose.model("fruit",Schema);
    //生成一個document
    let apple = new Model({
        category:'apple',
        name:'apple'
    });
    //存放資料
    apple.save((err,apple)=>{
        if(err) return console.log(err);
        apple.eat();
        //查詢資料
        Model.find({name:'apple'},(err,data)=>{
            console.log(data);
        })
    });
})

到這裡, 實際上, mongoose我們已經就學會了. 剩下就是看一看官方文件的API–CRUD相關操作. 如果,大家覺得意猶未盡的話,可以繼續看下面的深入淺出. 而且, 下面會附上實際應用中, mongoose的寫法.

深入淺出mongoose

這裡,我們根據上面的3個概念深入的展開一下.

Schema

這實際上是,mongoose中最重要的一個theroy. schema 是用來定義 documents的基本欄位和集合的. 在mongoose中,提供了Schema的類。 我們可以通過例項化他, 來實現建立 Schema的效果. 而不需要每次呼叫 mongoose.Schema()這個醜陋的API.

// from mongoose author
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var blogSchema = new Schema({
  title:  String,
  author: String,
  body:   String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs:  Number
  }
});

Schema 之所以能夠定義documents, 是因為他可以限制你輸入的欄位及其型別. mongoose支援的基本型別有:

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array

其中, Mixed和ObjectId是mongoose中特有的,ObjectId實際上就是**_id**的一個對映. 同樣,mongoose裡面有著和所有大眾資料庫一樣的東西. 索引 – indexs

mongoose 設定索引

這裡設定索引分兩種,一種設在Schema filed, 另外一種設在 Schema.index 裡.

//在field 設定
var animalSchema = new Schema({
  name: String,
  type: String,
  tags: { type: [String], index: true } 
});
//在Schema.index中設定.
animalSchema.index({ name: 1, type: -1 }); 

//1 表示正序, -1 表示逆序

實際上,兩者效果是一樣的. 看每個人的喜好了. 不過推薦直接在Schema level中設定, 這樣分開能夠增加可讀性. 不過,官方給出了一個建議, 因為在建立欄位時, 資料庫會自動根據自動排序(ensureIndex). 有可能嚴重拖慢查詢或者建立速度,所以一般而言,我們需要將該option 關閉.

mongoose.connect('mongodb://user:[email protected]:port/database', { config: { autoIndex: false } });  //真心推薦
// or  
mongoose.createConnection('mongodb://user:[email protected]:port/database', { config: { autoIndex: false } });  //不推薦
// or
animalSchema.set('autoIndex', false);  //推薦
// or
new Schema({..}, { autoIndex: false }); //懶癌不推薦

另外, Schema 另一大特色就是其methods. 我們可以通過定義其methods,訪問到實際上的所有內容.

定義Schema.methods

使用的方法很簡單,就是使用 .methods即可.

// 定義一個schema
var freshSchema = new Schema({ name: String, type: String });

// 新增一個fn. 
animalSchema.methods.findSimilarTypes = function (cb) {
  //這裡的this指的是具體document上的this
  return this.model('Animal').find({ type: this.type }, cb);
}
// 實際上,我們可以通過schema繫結上,資料庫操作的所有方法.
// 該method實際上是繫結在 例項的 doc上的

定義完methods和property之後, 就到了生成Model的階段了.

例項Model

這裡同樣很簡單,只需要 mongoose.model() 即可.

//生成,model 類. 實際上就相當於我們的一個collection
var Animal = mongoose.model('Animal', animalSchema);
var dog = new Animal({ type: 'dog' });

但是, 這裡有個問題. 我們在Schema.methods.fn 上定義的方法,只能在 new Model() 得到的例項中才能訪問. 那如果我們想,直接在Model上呼叫 相關的查詢或者刪除呢?

繫結Model方法

同樣很簡單,使用 statics 即可.

// 給model新增一個findByName方法
animalSchema.statics.findByName = function (name, cb) {
  //這裡的this 指的就是Model
  return this.find({ name: new RegExp(name, 'i') }, cb);
}

var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function (err, animals) {
  console.log(animals);
});

Mongoose 還有一個super featrue-- virtual property 該屬性是直接設定在Schema上的. 但是,需要注意的是,VR 並不會真正的存放在db中. 他只是一個提取資料的方法.

//schema基本內容
var personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
});

// 生成Model
var Person = mongoose.model('Person', personSchema);

//現在我們有個需求,即,需要將first和last結合輸出.
//一種方法是,使用methods來實現
//schema 新增方法
personSchema.methods.getName = function(){
    return this.first+" "+this.last;
}

// 生成一個doc
var bad = new Person({
    name: { first: 'jimmy', last: 'Gay' }
});

//呼叫
bad.getName();

但是,像這樣,僅僅這是為了獲取一個屬性, 實際上完全可以使用虛擬屬性來實現.

//schema 新增虛擬屬性
personSchema.virtual('fullName').get(function(){
    return this.first+" "+this.last;
})
//呼叫
bad.fullName;  //和上面的方法的結果是完全一致的

而且,經過測試, 使用fn實現的返回,比VR 要慢幾十倍. 一下是測試結果:

    console.time(1);
    bad.getName();
    console.timeEnd(1);
    console.time(2);
    bad.fullName;
    console.timeEnd(2);
    
    //結果為:
    1: 4.323ms;  //method
    2: 0.253ms  // VR

最後再補充一下,Schema中初始化的相關引數.

Schema引數 在 new Schema([options]) 中,我們需要設定一些相關的引數.

  • safe: 用來設定安全模式. 實際上,就是定義入庫時資料的寫入限制. 比如寫入時限等.
 //使用安全模式. 表示在寫入操作時,如果發生錯誤,也需要返回資訊.
 var safe = true;
new Schema({ .. }, { safe: safe });

// 自定義安全模式. w為寫入的大小範圍. wtimeout設定寫入時限. 如果超出10s則返回error
var safe = { w: "majority", wtimeout: 10000 };
new Schema({ .. }, { safe: safe });
  • toObject: 用來表示在提取資料的時候, 把documents 內容轉化為Object內容輸出. 一般而言只需要設定getters為true即可.
schema.set('toObject', { getters: true });
var M = mongoose.model('Person', schema);
var m = new M({ name: 'Max Headroom' });
//實際打印出來的就是一個Object型別
console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
  • toJSON: 該是和toObject一樣的使用. 通常用來把 documents 轉化為Object. 但是, 需要顯示使用toJSON()方法,否則,不會起作用. 實際上,沒什麼卵用…

看一下總結圖譜: schema

看完schema之後,我們需要了解一下 model的內容.

Model

實際上,Model才是操作資料庫最直接的一塊內容. 我們所有的CRUD就是圍繞著Model 展開的. ok. 還記得,我們是怎樣建立一個model的嗎?

model的建立

model的建立實際上就是方法的copy. 將schema上的方法,copy到model上. 只是copy的位置不一樣, 一部分在prototype上, 一部分在constructor中.

//from mongoosejs
var schema = new mongoose.Schema({ name: 'string', size: 'string' });
var Tank = mongoose.model('Tank', schema);

這裡,我們一定要搞清楚一個東西. 實際上, mongoose.model裡面定義的第一個引數,比如’Tank’, 並不是資料庫中的, collection. 他只是collection的單數形式, 實際上在db中的collection是’Tanks’.

ok, 我們現在已經有了一個基本的Model. 但並沒有什麼x用. 接下來, 正式到了 dry goods(乾貨) 時間.

model 的子文件操作 這個就厲害了. 本來mongodb是沒有關係的. 但是, mongoose提供了children欄位. 讓我們能夠輕鬆的在表間建立關係. 現在,我們來建立一個子域:

var childSchema = new Schema({ name: 'string' });

var parentSchema = new Schema({
  children: [childSchema]   //指明sub-doc的schema
});
//在建立中指明doc
var Parent = mongoose.model('Parent', parentSchema);
var parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] })
parent.children[0].name = 'Matthew';
parent.save(callback);

現在, 我們就已經建立了3個table. 一個parent 包含了 兩個child 另外,如果我們想要查詢指定的doc。 則可以使用 id()方法.

var doc = parent.children.id(id);

子文件的CRUD, 實際上就是陣列的操作, 比如push,unshift,remove,pop,shift等

parent.children.push({ name: 'Liesl' });

mongoose還給移除提供了另外一個方法–remove:

var doc = parent.children.id(id).remove();

如果你忘記新增子文件的話,可以在外圍新增, 但是欄位必須在Schema中指定

var newdoc = parent.children.create({ name: 'Aaron' });

model的CRUD操作

model的建立 關於model的建立,有兩種方法, 一種是使用例項建立,另外一種是使用Model類建立.

var Tank = mongoose.model('Tank', yourSchema);

var small = new Tank({ size: 'small' });
//使用例項建立
small.save(function (err) {
  if (err) return handleError(err);
  // saved!
})

//使用Model類建立
Tank.create({ size: 'small' }, function (err, small) {
  if (err) return handleError(err);
  // saved!
})

上面已經完美的介紹建立的方法了. 另外,官方給出一個提醒: 由於mongoose, 會自身連線資料庫並斷開. 如果你手動連線, 則建立model的方式需要改變.

// 自己並沒有開啟連線:
//注意,這裡只是連線,並沒有建立connection
mongoose.connect('mongodb://localhost:27017/test'); 

//手動建立連線:
var connection = mongoose.createConnection('mongodb://localhost:27017/test');
var Tank = connection.model('Tank', yourSchema);

然後, 下面的API還是一樣的. 實際上,我們一般常用的寫法為:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
//設定連線位置
mongoose.connect('mongodb://localhost:27017/test');
var schema = new mongoose.Schema({ name: 'string', size: 'string' });
var Tank = mongoose.model('Tank', schema);
var small = new Tank({ size: 'small' });
//使用例項建立
small.save(function (err) {
  if (err) return handleError(err);
  console.log('建立成功');
})

這樣,就不用自己去手動管連線的問題了. 如果你,在後面想手動新增欄位的話,可以使用.set方法.

// 一個key/valye
doc.set(path, value)
//很多key/value
doc.set({
    path  : value,
    path2 : {
       path  : value
    }
})

model的query model的查詢主要提供了以下的API,給我們進行操作. find, findById, findOne, or where 在mongodb中, query返回的資料格式一般都是為JSON的. 這點需要注意.

事實上,在mongoose中,query資料 提供了兩種方式.

  • callback: 使用回撥函式, 即, query會立即執行,然後返回到回撥函式中.
Person.findOne({ 'name.last': 'Ghost' }, 'name occupation', function (err, person) {
  if (err) return handleError(err);
 // get data
})
  • query: 使用查詢方法,返回的物件. 該物件是一個Promise, 所以可以使用 chain 進行呼叫.最後必須使用exec(cb)傳入回撥進行處理. cb 是一個套路, 第一個引數永遠是err. 第二個就是返回的資料。
Person.
  find({
    occupation: /host/,
    'name.last': 'Ghost',
    age: { $gt: 17, $lt: 66 },
    likes: { $in: ['vaporizing', 'talking'] }
  }).
  limit(10).
  sort({ occupation: -1 }).
  select({ name: 1, occupation: 1 }).
  exec(callback);
  
//如果沒有查詢到,則返回[] (空陣列)
// 如果你使用findOne, 沒有的話則會返回 null

童鞋, 你覺得我會推薦哪種呢?

上面4個API, 3個使用方式都是一樣的, 另外一個不同的是where. 他一樣是用來進行query. 只是,寫法和find系列略有不同.

where簡介 where的API為: Model.where(path, [val]) path實際上就是欄位, 第二個引數.val表示可以用來指定,path = val的資料內容, 你也可以不寫, 交給後面進行篩選. 看一下對比demo吧:

User.find({age: {$gte: 21, $lte: 65}}, callback);
//等價於:
User.where('age').gte(21).lte(65).exec(callback);

從上面的query中,我們可以看到有許多fn, 比如gte,lte,$gte,$lte. 這些是db提供給我們用來查詢的快捷函式. 我們可以參考, mongoose給的參考: query Helper fn 這裡,我們簡要的瞭解下,基本的快捷函式.

name effect
select 新增需要顯示的欄位,需要的欄位在欄位後加上:1,不需要的加上0;<br/>query.select({ a: 1, b: 0 }); //顯示a欄位, 隱藏b欄位<br/>不能和distinct方法一起使用
distinct 用來篩選不重複的值或者欄位<br/>distinct(field). //篩選指定不重複欄位的資料
$lt,$lte,$gt,$gte. 分別對應: <,<=,>,>=. 該欄位是用在condition中的.如果,你想要鏈式呼叫,則需要使用<br/>lt,lte,ge,gte.<br/>eg:<br/> model.find({num:{$gt:12}},cb)<br/>model.where(‘num’).gt(12).exec(cb)
$in 查詢包含鍵值的文件,<br/>model.find({name:{$in:[“jimmy”,“sam”]}}) //相當於匹配 jimmy或者sam
$nin 返回不匹配查詢條件文件,都是指定陣列型別<br/>model.find({name:{$nin:[“jimmy”,“sam”]}})
$ne 表示不包含指定值<br/>model.find({name:{$ne:“sam”}})
$or 表示或查詢<br/>model.find({$or:[{ color: ‘red’ }, { status: ‘emergency’ }]})
$exits 表示鍵值是否存在;<br/>model.find({name:{$exits:true}})
$all 通常用來匹配數組裡面的鍵值,匹配多個值(同時具有)<br/>$all:[“apple”,“banana”,“peach”]}
$size 用來查詢陣列的長度值<br/>model.find({name:{$size:3}}); 匹配name的陣列長度為3
$slice 用來獲取陣列欄位的內容:<br/>query.slice(‘comments’, 5)

ok~ 這上面就是比較常用的快捷函式. 另外還有一些遊標集合的處理方法: 常用的就3個, limit,skip,sort.

  • **limit:**用來獲取限定長度的內容.
query.limit(20); //只返回前20個內容
  • skip: 返回,跳過指定doc後的值.
query.skip(2);
  • sort: 用來設定根據指定欄位排序. 可以設定為1:升序, -1:降序.
query.sort({name:1,age:-1});

實際上, 關於query,我們需要了解的也就差不多了.

我們接下來,來看一下remove. mongoose remove 操作

官方提供的API,就是remove. 同樣,移除的效果,我們可以使用兩種方式實現。 一是回撥函式, 二是, 鏈式呼叫.

 Model.find().remove({ name: 'Anne Murray' }).remove(fn);
 //或者直接添加回調
 Model.find().remove({ name: 'Anne Murray' },cb)

另外,我們可以直接在Model上呼叫. 因為remove也是Schema定義的statics方法. 而且, remove返回一個Promise物件

product.remove().then(function (product) {
   ...
});
//或者直接傳入回撥
Tank.remove({ size: 'large' }, function (err) {
  if (err) return handleError(err);
  // removed!
});

最後,我們再看一下 update. 然後mongoose就基本結束了 update操作: 這裡,我只說一下API就好. 因為update 比起上面來說,還是比較簡單的. Model.update(conditions, doc, [options], [callback])

  • conditions: 就是query. 通過query獲取到指定doc
  • doc: 就是用來替換doc內容的值.
  • options: 這塊需要說一下.
    • safe (boolean) 是否開啟安全模式 (default for true)
    • upsert (boolean) 如果沒有匹配到內容,是否自動建立 ( default for false)
    • multi (boolean) 如果有多個doc,匹配到,是否一起更改 ( default for false)
    • strict (boolean) 使用嚴格模式(default for false)
    • overwrite (boolean) 匹配到指定doc,是否覆蓋 (default for false)
    • runValidators (boolean): 表示是否用來啟用驗證. 實際上,你首先需要寫一個驗證. 關於如果書寫,驗證大家可以參考下文, validate篇(default for false)
Model.update({age:18}, { $set: { name: 'jason borne' }}, {multi:true}, function (err, raw) {
  if (err) return handleError(err);
  console.log('raw 就是mongodb返回的更改狀態的falg ', raw);
  //比如: { ok: 1, nModified: 2, n: 2 }
});

其中的$set是,用來指明更新的欄位.另外,mongoose還提供了一個:findByIdAndUpdate(id,doc[,options][,callback]); 方法. 關於mongoose的更新helper 函式. 童鞋們可以參考一下.mongoose官方文件.

validation

說完了,mongoose的body之後. 我們接著來看一下,官方給mongoose穿上的漂亮的衣服. 其中一件,比較吸引人的是–validation. 在你save資料之前, 你可以對資料進行一些列的validation. 來防止某天你傻不拉幾的把資料完整性給破壞了. mongoose貼心的提供了幾個built-in的驗證函式.

  • required: 表示必填欄位.
new Schema({
 name: {
    type:String,
    required:[true,"name 是必須的"] //第二個引數是錯誤提示資訊
 }
})
  • min,max: 用來給Number型別的資料設定限制.
 var breakfastSchema = new Schema({
      eggs: {
        type: Number,
        min: [6, 'Too few eggs'],
        max: 12
      }
});
  • enum,match,maxlength,minlength: 這些驗證是給string型別的. enum 就是列舉,表示該屬性值,只能出席那那些. match是用來匹配正則表示式的. maxlength&minlength 顯示字串的長度.
new Schema({
    drink: {
        type: String,
        enum: ['Coffee', 'Tea']
      },
     food:{
        type: String,
        match:/^a/,
        maxlength:12,
        minlength:6
    }
})

mongoose提供的helper fn就是這幾種, 如果你想定製化驗證. 可以使用custom validation.

new Schema({
 phone: {
        type: String,
        validate: {
          validator: function(data) {
            return /\d{3}-\d{3}-\d{4}/.test(data);
          },
          message: '{VALUE} is not a valid phone number!' //VALUE代表phone存放的值
        },
        required: [true, 'User phone number required']
      }
})

另外,還可以額外新增驗證.

  var toySchema = new Schema({
      color: String,
      name: String
    });

   var validator = function (value) {
      return /blue|green|white|red|orange|periwinkle/i.test(value);
    };
    toySchema.path('color').validate(validator,
      'Color `{VALUE}` not valid', 'Invalid color');

現在,我們已經設定了validation. 但是你不啟用,一樣沒有什麼卵用. 實際上, 我們也可以把validation當做一箇中間件使用. mongoose 提供了兩種呼叫方式. 一種是內建呼叫, 當你使用.save方法時,他會首先執行一次儲存方法.

cat.save(function(error) {
//自動執行,validation
});

另外一種是,手動驗證–指定validate方法.

//上面已經設定好user的欄位內容.
  user.validate(function(error) {
    //error 就是驗證不通過返回的錯誤資訊
     assert.equal(error.errors['phone'].message,
        '555.0123 is not a valid phone number!');
    });
});

事實上, 在validate時, 錯誤的返回資訊有以下4個欄位: kind, path, value, and message;

  • kind: 用來表示驗證設定的第二個引數. 一般不用
phone: {
        type: String,
        validate: {
          validator: function(data) {
            return /\d{3}-\d{3}-\d{4}/.test(data);
          },
          message: '{VALUE} is not a valid phone number!', //VALUE代表phone存放的值
          kind: "invalid phone"
        }
})
  • path: 就是欄位名
  • value: 你設定的錯誤內容
  • message: 提示錯誤資訊 看一個整體demo吧:
 var validator = function (value) {
      return /blue|green|white|red|orange|periwinkle/i.test(value);
    };
    Toy.schema.path('color').validate(validator,
      'Color `{VALUE}` not valid', 'Invalid color'); //設定了message && kind

    var toy = new Toy({ color: 'grease'});

    toy.save(function (err) {
      // err is our ValidationError object
      // err.errors.color is a ValidatorError object
      assert.equal(err.errors.color.message, 'Color `grease` not valid'); //返回message
      assert.equal(err.errors.color.kind, 'Invalid color');
      assert.equal(err.errors.color.path, 'color');
      assert.equal(err.errors.color.value, 'grease');
      assert.equal(err.name, 'ValidationError');
      //訪問color 也可以直接上 errors["color"]進行訪問.
    });

在Model.update那一節有個引數–runValidators. 還沒有詳細說. 這裡, 展開一下. 實際上, validate一般只會應用在save上, 如果你想在update使用的話, 需要額外的trick,而runValidators就是這個trick.

var opts = { runValidators: true };
    Test.update({}, update, opts, function(error) {  //額外開啟runValidators的驗證
      // There will never be a validation error here
    });

我們來看一下基本總結吧: valdation

population

originally, mongodb 本來就是一門非關係型資料庫。 但有時候,我們又需要聯合其他的table進行資料查詢。 這時候, 一般的做法就是實現兩次查詢,效率我就呵呵了.
此時, mongoose 說了一句: 麻麻, 我已經都把髒活幫你做好了. 感動~ 有木有~ 這就是mongoose提供的 population. 用來連線多表資料查詢. 一般而言, 我們只要提供某一個collection的_id , 就可以實現完美的聯合查詢. population 用到的關鍵字是: ref 用來指明外聯的資料庫的名字. 一般,我們需要在schema中就定義好.

var mongoose = require('mongoose')
  , Schema = mongoose.Schema
  
var personSchema = Schema({
  _id     : Number,
  name    : String,
  age     : Number,
  stories : [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

var storySchema = Schema({
  _creator : { type: Schema.Types.ObjectId, ref: 'Person' },
  title    : String
});

這裡就指明瞭, 外聯資料表的應用關係 personSchema <stories> By _id => Story storySchema <_creator> By _id => Person 實際上, 就是通過_id的相互索引即可. 這裡需要說明的是, _id 應該是某個具體model的id.

我們來看一下, 接下來應該如何利用population實現外聯查詢.

const sam = new Person({
    name: 'sam',
    _id: 1,
    age: 18,
    stories: []
});
sam.save((err,sam)=>{
    if(err) return err;
    let story = new Story({
        _creator:sam._id,
        title:"喜劇之王"
    })
})

Story.findOne({title:"喜劇之王"}).populate('_creator').exec((err,story)=>{
    if(err)console.log(err);
    console.log(story._creator.name);
})
//使用populate來指定,外聯查詢的欄位, 而且該值必須是_id才行

現在es6時代的來臨, generator , async/await 盛行. 也帶來另外一種書寫方式–middleware. 在mongoose也有這個hook, 給我們使用. (說實話, 有點像AOP)

mongoose && middleware

mongoose裡的中介軟體,有兩個, 一個是pre, 一個是post.

  • pre: 在指定方法執行之前繫結。 中介軟體的狀態分為 parallel和series.
  • post: 相當於事件監聽的繫結

這裡需要說明一下, 中介軟體一般僅僅只能限於在幾個方法中使用. (但感覺就已經是全部了)

  • doc 方法上: init,validate,save,remove;
  • model方法上: count,find,findOne,findOneAndRemove,findOneAndUpdate,update

pre

我們來看一下,pre中介軟體是如何繫結的.

// series執行, 序列
var schema = new Schema(..);
schema.pre('save', function(next) {
 // exe some operations
 this.model.
  next();  //  這裡的next()相當於間執行權給下一個pre
});

在你呼叫 model.save方法時, 他會自動執行pre. 如果你想並行執行中介軟體, 可以設定為:

schema.pre('save', true, function(next, done) {
  // 並行執行下一個中介軟體
  next();
});

post

相當於繫結啦~ post會在指定事件後觸發

schema.post('save', function(doc) {
 //在save完成後 觸發.
  console.log('%s has been saved', doc._id);
});

當save方法呼叫時, 便會觸發post繫結的save事件. 如果你綁定了多個post。 則需要指定一下中介軟體順序.

schema.post('save', function(doc, next) {
  setTimeout(function() {
    console.log('post1');
    next();
  }, 10);
});

schema.post('save', function(doc, next) {
  console.log('post2');
  next();
});

實際上,post觸發的時間為:

var schema = new Schema(..);
schema.post('save', function (doc) {
  console.log('this fired after a document was saved');
});

var Model = mongoose.model('Model', schema);

var m = new Model(..);
m.save(function (err) {
  console.log('this fires after the `post` hook');
});

另外,在post和find中, 是不能直接修改doc上的屬性的. 即,像下面一樣的,沒有效果

articleSchema.post('find',function(docs){
    docs[1].date = 1
})
docs[1].date 的值還是不變

不過可以使用虛擬屬性,進行操作.