Practical Node.js (2018版) 第7章:Boosting Node.js and Mongoose

參考:博客 https://www.cnblogs.com/chentianwei/p/10268346.htmljavascript

參考: mongoose官網(https://mongoosejs.com/docs/models.html)html

參考: 英文:Boosting Node.js和MongoDB with Mongoose前端


 

 

簡介:mongoose

Mongoose is a fully developed object document mapping (ODM) library for Node.js and MongoDB. java

ODM的概念對應sql的ORM,就是ruby on rails中的activerecord那因層。node

activerecord包括migrations, Validations, associations, Query interface, 對應mvc框架中的Models。git

ORMObject-Relational Mappiing。程序員

ODM的做用,定義數據庫的數據格式schema, 而後經過它取數據,把數據庫中的document映射成程序中的一個對象。這個對象有save, update的系列方法,有tilte, author等系列屬性。github

在調用這些方法時,odm會根據你調用時使用的條件,轉化成mongoDb Shell語言,幫你發送出去。web

天然,在程序內使用鏈式調用,比手寫數據庫語句更靈活也方便。正則表達式

 

例子:

//先安裝好MongoDb和Node.js
$ npm install mongoose

// getting-started.js
var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost:27017/test');

db.on('error', console.error.bind(console, "connection error"))

db.once('open', function() {
  //當鏈接成功後,寫Schema, model, 寫實例並保存到數據庫。
})

在db.once內的例子1

var userSchema = new mongoose.Schema({
  user: {
    username: String,
    password:  String
  }
})

var User = mongoose.model('user', userSchema)
var frank = new User({
  user: {
    username: 'Frank',
    password: '123456'
  }
})

frank.save((err, frank) => {
  console.log('save success!')
  console.log(frank.user)
}) 

在db.once()的例子2

  //構建一個Schema
  var kittySchema = new mongoose.Schema({
    name: String
  });
  // 寫一個方法
  kittySchema.methods.speak = function () {
    var greeting = this.name
      ? "Meow name is " + this.name
      : "I don't have a name";
    console.log(greeting);
  }
  // 生成一個model
  var Kitten = mongoose.model('Kitten', kittySchema);
  // 實例化一個對象
  var fluffy = new Kitten({ name: 'fluffy' });
  // 經過mongoose寫入數據庫
  fluffy.save((err, fluffy) => {
    if (err) {
      return console.error(err)
    }
    fluffy.speak()
  })

⚠️:此時已經將fluffy對象保存到mongodb://localhost:27017/test的Kitten model內。

即將一個document,保存到test數據庫的kittens collection中。

model自動建立了kittens這個collection。(自動添加了s)

⚠️注意:此時mongoDb尚未建立kittens

在建立一個實例並執行save方法,test數據庫纔會建立了kittens collections和documents。

 

能夠對比使用node.js mongodb driver的代碼。

var MongoClient = require('mongodb').MongoClient,
    assert=require('assert');
var url = 'mongodb://localhost:27017/myproject';
MongoClient.connect(url,function(err,db){
    assert.equal(null,err);
    console.log("成功鏈接到服務器");
    insertDocuments(db,function(){
        db.close();
    });
   // db.close();
});
var insertDocuments = function(db,callback){
    var collection = db.collection('documents');
    collection.insertMany([
        {a:1},
        {a:2},
        {a:3}
    ],function(err,result){
        assert.equal(err,null);
        assert.equal(3,result.result.n);
        assert.equal(3,result.ops.length);
        console.log("成功插入3個文檔到集合!");
        callback(result);
 });
}

 

上面代碼是專爲Node.js提供的驅動程序代碼和mongDB shell語言相似。

而,用mongoose定位於使用關係型的數據結構schema,來構造你的app data。

它包括內置的類型構件, 驗證, 查詢,業務邏輯勾子和更多的功能,開箱即用out of the box!

 

mongoose把你使用Node.js驅動代碼本身寫複雜的驗證,和邏輯業務的麻煩,簡單化了。

mongoose創建在MongoDB driver之上,讓程序員能夠model 化數據。

兩者各有優缺點:

mongoose須要一段時間的學習和理解。在處理某些特別複雜的schema時,會遇到一些限制。

但直接使用Node.js的驅動代碼,在你進行數據驗證時會寫大量的代碼,並且會忽視一些安全問題。



 

Node.js practical 第七章

不喜歡使用mongoose進行復雜的query,而是使用native driver。

Mongoose的缺點是某些查詢的速度較慢。

固然Mongoose的優勢不少。由於ODM(object document mapping)是現代軟件編程的重要部分!

特別是企業級的engineering。

 

主要優點,就是從database中,提取每件事:程序代碼只和object和它們的methods交互。

ODM容許指定:不一樣類型的對象和把業務邏輯放在類內(和那些對象相關)之間的關係relationships.

 

另外,內建的驗證和類型type casting能夠擴展和客制。

當Mongoose和Express.js一塊兒使用時, Mongoose讓stack真正地擁護MVC理念。

 

Mongoose 使用相似Mongo shell, native MongoDB driver的交互方式。

 

Buckle up!本章將要討論:

  • Mongoose installation
  • Connection establishment in a standalone Mongoose script
  • Mongoose schemas
  • Hooks for keeping code organized
  • Custom static and instance methods
  • Mongoose models
  • Relationships and joins with population
  • Nested documents
  • Virtual fields
  • Schema type behavior amendment
  • Express.js + Mongoose = true MVC

 

 

安裝

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true});
//一個mongoose鏈接實例
var db = mongoose.connection;

db.once('open', () => {
//...
})

 

和native driver不同,咱們無需等待established connection, 只須要把全部的代碼放入open()回調內。

不放入open()也能夠,默認使用buffer。使用open(),確保鏈接了服務器。

⚠️官方文檔原文的解釋:

Mongoose lets you start using your models immediately, without waiting for mongoose to establish a connection to MongoDB.

不管是否鏈接上服務器的MongoDB數據庫,均可以立刻使用model。

mongoose.connect('mongodb://localhost:27017/myapp', {useNewUrlParser: true});
var Schema = mongoose.Schema var MyModel = mongoose.model('Test', new Schema({ name: String })); // Works MyModel.findOne(function(error, result) { /* ... */ });

 

That's because mongoose buffers model function calls internally. This buffering is convenient, but also a common source of confusion. Mongoose will not throw any errors by default if you use a model without connecting.

這是由於mongoose內部地緩衝了模型函數調用。這個緩衝很是的方便,但也是一個常見的source困惑。

由於若是在沒有鏈接的狀況下,你使用model,Mongoose默認不會拋出❌,

//一個腳本
const mongoose = require('mongoose')

var MyModel = mongoose.model('Test', new Schema({ name: String}));
//查詢的代碼會掛起來,指定mongoose成功的鏈接上。
MyModel.findOne(function(error, result) { /*...*/});

setTimeout(function() {
  mongoose.connect('mongodb://localhost:27017/myapp', {useNewUrlParser: true})
}, 6000)

 


 

 

在一個mongoose腳本創建一個鏈接

鏈接的URI結構:(一個string)

mongodb://username:password@host:port/database_name

 

默承認以以下使用,host是localhost, port是27017, 數據庫名字是test, 不設置username和password:

mongoose.connect('mongodb://localhost:27017/test', {useMongoClient: true})
mongoose.Promise = global.Promise

 

Promise這行讓mongoose可使用native ES6 promise 。也可使用其餘的promise implementation 。

Mongoose.prototype.Promise //The Mongoose Promise constructor。

Options對象

connect(url, options)。 options是一個對象,裏面是關於鏈接的屬性設置。具體見官方文檔。徹底支持原生Node.js driver。

 

Model

下一步: 一個重要的差異(不一樣於Mongoskin和其餘輕量型MongoDB庫):

建立一個model, 使用model()函數並傳遞一個string和一個schema

const Book = mongoose.model("Book", {name: String})

 

⚠️這裏沒有使用new mongoose.Schema()

 

如今配置語句結束,咱們建立a document表明Book model 的實例:

const oneBook = new Book({name: 'Practical Node.js'})

 

Mongoose documents有很是方便的內置方法:validate, isNew, update

(https://mongoosejs.com/docs/api.html#Document)

⚠️留心這些方法只能用在document上,不能用在collection或model上。

 

docuement是a model的實例, 而a model有點抽象,相似real MongoDB collection。

可是, 它由一個schema支持, 而且做爲一個Node.js class(及額外的方法和屬性)存在。

Models are fancy constructors compiled from Schema definitions.

 

一般,咱們不直接地使用Mongoose collections, 咱們只經過models操做數據。

一些主要的model方法和native MongDB driver相似: find(), insert(), save()等等。

 

爲了把一個docuemnt存入數據庫,使用document.save()

這個方法是異步的asynchronous。所以添加一個callback或者promise或者async/await函數。

執行下面的腳本代碼⚠️先打開MongoDB,server。

const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/test')
mongoose.Promise = global.Promise
const Book = mongoose.model("Book", {name: String})

const oneBook = new Book({name: "Hello world!"})

oneBook.save((err, result) => { if (err) { console.err(err) process.exit(1) } else { console.log("Saved:", result) process.exit(0) } })

 


 

 

Mongoose Schemas

 

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

Mongoose開始於一個schema. 每一個scheme映射到一個MongoDB collection並定義這個collection中的document的外形。

var mongoose = require('mongoose');
var blogSchema = new mongoose.Schema({
  title: String,
  comments: [{body: String, date: Date}],
  date: { type: Date, default: Date.now},
  hidden: Boolean
})

//add()方法,用於添加屬性,參數是一個key/value對象, 或者是另外一個Schema.
//add()能夠鏈式調用。
blogSchema.add({author: String})

 

每一個key在咱們的documents內定義了一個屬性並給予一個相關的SchemaType。

key也能夠是嵌套的對象。

 

SchemaTypes:

  • String, Number, Date, Boolean
  • Buffer: a Node.js binary type(圖片images, PDFs, archives等等)
  • Mixed: 一種"anything goes"類型。任意類型的數據
  • ObjectId: _id key 的類型。
  • Array
  • map

Schema不僅定義document的結構和屬性,也定義document的實例方法,靜態Model方法, 混合的compond indexes, 文檔hooks 調用middleware。

 

建立model

爲了使用咱們的schema定義,須要轉化blogSchema進入一個Model:

var Blog = mongoose.model('Blog', blogSchema)

 

Models的實例是documents。Documents有內建的實例方法。

 

Instance methods

經過schema定義客製化的實例方法:

var animalSchema = new Schema({ name: String, type: String })

// 分配一個函數給methods對象
animalSchema.methods.findSimilarTypes = function(callback) { return this.model("Animal").find({ type: this.type }, callback) }

 

var Animal = mongoose.model('Animal', animalSchema)
var dog = new Animal({type: 'dog'})
// 存入數據庫
dog.save((err, dog)
=> { console.log("save success!") }) // dog document使用自定義的方法 dog.findSimilarTypes(function(err, dogs) { console.log("yes", dogs); // yes [ { _id: 5c45ba13aaa2f74d3b624619, type: 'dog', __v: 0 } ] });

 

 

Statics

給一個Model增長一個靜態方法。

把一個函數分配給animalSchema的statics對象。

若是把一個Model當作一個類,那麼靜態方法就是這個類的類方法。

animalSchema.statics.findByName = function(name, callback) {
  return this.find({name: new RegExp(name, "i") }, callback)
}

var Animal = mongoose.model("Aniaml", animalSchema)
Animal.findByName("fido", function(err, animals) {
  console.log("result: ", animals)
}) 

⚠️,聲明statics,不能使用箭頭函數。由於箭頭函數明確地防止綁定this。

也可使用Schema.static(name, funtion)方法

var schema = new mongoose.Schema(..);

schema.static('findByName', function(name, callback) => {
  return this.find({name: name}, callback)
})

使用{name: fn, name:fun, ...}做爲惟一參數:

若是把一個hash(內含多個name/fn 對兒),做爲惟一的參數傳遞給static(), 那麼每一個name/fn對兒將會被增長爲statics靜態方法。

bookSchema.static({ // Static methods for generic, not instance/document specific logic
  getZeroInventoryReport: function(callback) {
    // Run a query on all books and get the ones with zero inventory
    // Document/instance methods would not work on "this"
    return callback(books)
  },
  getCountOfBooksById: function(bookId, callback){
    // Run a query and get the number of books left for a given book
    // Document/instance methods would not work on "this"
    return callback(count)
  }
})

 

 

Query Helpers

能夠增長query helper functions, 相似實例方法(❌?這句不是很明白,哪裏相似了?),

可是for mongoose queries。

Query helper methods 讓你擴展mongoose的鏈式查詢builder API。chainable query builder API.

  animalSchema.query.byName = function(name) {
    return this.where({ name: new RegExp(name, 'i') });
  };

  var Animal = mongoose.model('Animal', animalSchema);

  Animal.find().byName('fido').exec(function(err, animals) {
    console.log(animals);
  });

⚠️由上可見query helper方法是Model調用的。因此原文 like instance methods 這句不明白。

 

indexes

MongDB支持第二個indexes.

使用mongoose,定義indexes的方法有2個:

  • 在定義一個Schema時
  • 使用Schema對象的index()方法。(主要用於組合式索引)
var animalSchema = new mongoose.Schema({
  name: String,
  type: String,
  tags: { type: [String], index: true}
})

animalSchema.index({ name: 1, type: -1})

 

 

Virtuals

document的一個屬性。

 

Options

Schemas有一些選項配置,能夠用構建起或者用set()

new mongoose.Schema({..}, options)

// or
var schema = new mongoose.Schema({..})
schema.set(option, value)

 

Pluggable

Mongoose schemas是插件方式的, 便可以經過其餘程序的schemas進行擴展。

(具體使用點擊鏈接)

 

Hooks for Keeping Code Organized

假如:在有大量關聯的對象的複雜應用內,咱們想要在保存一個對象前,執行一段邏輯。

使用hook,來儲存這段邏輯代碼是一個好方法。例如,咱們想要在保存一個book document前上傳一個PDF到web site:

//在一個schema上使用pre()鉤子:
booSchema.pre('save', (next) => { // Prepare for saving // Upload PFD return next() })

 pre(method, [options], callback)

第一個參數是method的名字

⚠️:鉤子和方法都必須添加到schemas上,在編譯他們到models 以前。也就是說,在調用mongoose.model()以前。

 


 

官方guide: SchemaTypes摘要

SchemaTypes處理definition of path defaults , 驗證, getterssetters,  查詢的默認field selection, 和Mongoose document屬性的其餘一些廣泛特徵。

 

你能夠把一個Mongoose Schema看做是Mongoose model的配置對象。

因而,一個SchemaType是一個配置對象,做爲一個獨立的屬性。

const schema = new Schema({ name: String });
schema.path('name') instanceof mongoose.SchemaType; // true
schema.path('name') instanceof mongoose.Schema.Types.String; // true
schema.path('name').instance; // 'String'
// 一個userSchema的userSchema.path("name"):
SchemaString {
  enumValues: [],
  regExp: null,
  path: 'name',
  instance: 'String',
  validators: [],
  getters: [],
  setters: [],
  options: { type: [Function: String] },
  _index: null }

我以爲:一個path相似關係型數據庫中的table中的一個field定義。

因此一個SchemaType,表達了一個path的數據類型, 它是不是getters/setters的模式。

 

一個SchemaType不等於一個Type。它只是Mongoose的一個配置對象。

mongoose.ObjectId !== mongoose.Types.ObjectId

它只是在一個schema內,對一個path的配置。

 

經常使用的SchemaTyps:

var schema = new mongoose.Schema({
  name:   String,
  binary:  Buffer,
  living:   Boolean,
  updated:  { type: Date, default: Date.now},
  age:        { type: Number, min: 18, max: 65},
  mixed:     Schema.Types.Mixed,
  _someId:   Schema.Types.ObjectId,
  array: []
})

 

數組的SchemaTypes:

var schema = new Schema({
  ofString: [String],
  ofNumber: [Number],
  ofDates: [Date],
  ofBuffer: [Buffer],
  ofBoolean: [Boolean],
  ofMixed: [Schema.Types.Mixed],
  ofObjectId: [Schema.Types.ObjectId],
  ofArrays: [[]],
  ofArrayOfNumbers: [[Number]],
//嵌套對象 nested: { stuff: { type: String, lowercase:
true, trim: true} }, map: Map, mapOfString: { type: Map, of: String } })

 

 

SchemaType Options:

var schema1 = new Schema({
  test: String // `test` is a path of type String
});

var schema2 = new Schema({
  // The `test` object contains the "SchemaType options"
  test: { type: String, lowercase: true } // `test` is a path of type string
});

 

你能夠增長任何屬性(你想要給你的SchemaType options)。 有許多插件客製化SchemaType options。

Mongoose有幾個內置的SchemaType options(具體見https://mongoosejs.com/docs/schematypes.html)

 

indexes

能夠用schema type options定義MongoDB indexes:

var schema2 = new Schema({
  test: {
    type: String,
    index: true,
    unique: true // Unique index. If you specify `unique: true`
    // specifying `index: true` is optional if you do `unique: true`
  }
});

 

不一樣的SchemaType有不一樣的options,具體見官方guide。

 


 

Mongoose Models

正如許多ORMs/ODMs, 在mongoose中,cornerstone object is a model。對象的基石是模塊。

把一個schema編譯進入一個model, 使用: 

mongoose.model(name, schema)

第一個參數name,是一個字符串,大寫字母開頭,一般這個string和對象字面量(聲明的變量名)同樣。

默認,Mongoose會使用這個model name的複數形式去綁定到一個collection name。

 

Models用於建立documents(實際的data)。使用構建器:

new ModelName(data)

 

Models又內建的靜態類方法相似native MongoDB方法,如find(), findOne(), update(), insertMany()

一些經常使用的model 方法:

  • Model,create(docs) 等同new Model(docs).save()
  • Model.remove(query, [callback(error)])。不能使用hooks。
  • Model.find(query, [fields], [options], [callback(error, docs)])
  • Model.update()
  • Model.populate(docs, options, [callback(err, doc)]), 填入。   
  • Model.findOne
  • Model.findById

注意⚠️,一部分model方法不會激活hooks, 好比deleteOne(),remove()。他們會直接地執行。

 

最經常使用的實例方法:

  • save()
  • toJSON([option]): 把document轉化爲JSON
  • toObject(): 把document轉化爲普通的JavaScript對象。
  • isModified([path]): True/false
  • doc.isNew: True/false
  • doc.id: 返回document id
  • doc.set():參數包括path, val, [type],  ⚠️path其實就是field名字key/value對兒的key。   
  • doc.validate(): 手動地檢測驗證(自動在save()前激活)

大多數時候,你須要從你的document獲得數據。

使用res.send()把數據發送到一個客戶端。

document對象須要使用toObject()和toJSON()轉化格式,而後再發送。

 


 

Document 

Retrieving

具體見:querying一章。

 

updating

可使用findById(), 而後在回調函數內修改查詢到的實例的屬性值。

Tank.findById(id, function (err, tank) {
  if (err) return handleError(err);

  tank.size = 'large'; //或者使用tank.set({ size: 'large' })
  tank.save(function (err, updatedTank) {
    if (err) return handleError(err);
    res.send(updatedTank);
  });
});

 

若是隻是想要把新的數據更新到數據庫,不返回,則可使用Model#updateOne()

Tank.update({_id: id}, { $set: {size: 'large'}}, callback)

 

若是如findById加上save(),返回新的數據,有更方便的方法: findByIdAndupdate()

配合使用res.send()

Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, { new: true }, function (err, tank) {
  if (err) return handleError(err);
  res.send(tank);
});

 

⚠️,findByIdAndUpdate不會執行hooks或者驗證,因此若是須要hooks和full documente validation,用第一種query而後save() it。

 

Validating

Documents在被保存前須要驗證,具體見validation 

 

重寫

.set(doc)方法,參數是另外一document的話,至關於重寫。

 

 

 


 

Relationships and Joins with Population

使用Model.populate()或者 Query.populate()

雖然,Node開發者不能查詢Mongo DB(on complex relationships), 可是經過Mongoose的幫助,開發者能夠在application layer作到這點。

在大型的程序中,documents之間又複雜的關係,使用mongoose就變得很方便了。

 

例如,在一個電子商務網站,一個訂單經過產品id,關聯產品。爲了獲得更多的產品信息,開發者須要寫2個查詢: 一個取order,另外一個取訂單的產品。

使用一個Mongoose query就能作到上面的2個查詢的功能。

 

Populate

Mongoose經過鏈接訂單和產品讓2者的關係變得簡單:Mongoose提供的一個功能,population。

這裏population涉及的意思相似related,即相關的,有聯繫的。

populations是關於增長更多的data到你的查詢,經過使用relationships。

它容許咱們從一個不一樣的collection取數據來fill填document的一部分。

 

好比咱們有posts和users,2個documents。Users能夠寫posts。這裏有2類方法實現這個寫功能:

  1. 使用一個collection,users collection有posts 數組field。這樣就只須要一個單獨的query,可是這種結構致使某些方法的被限制。由於posts不能被indexed or accessed separately from users.
  2. 或者使用2個collections(and models)。在這個案例,這種結構會更靈活一些。可是須要至少2個查詢,若是咱們想要取一個user和它的posts。

因而Mongoose提供了population,在這裏有用武之地了。

在user schema內引用posts。以後populate這些posts。爲了使用populate(), 咱們必須定義ref和model的名字:

const mongoose = require('mongoose')

const Schema = mongoose.Schema

const userSchema = new Schema({
  _id: Number,
  name: String,
 posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }]
})

⚠️,Schema.Types.ObjectId是一種SchemaType。 

實際的postSchema只加了一行代碼:

const postSchema = Schema({
  _creator: { type: Number, ref: 'User'},
  title: String,
  text: String
})

 

下面的幾行代碼是咱們建立models, 而後yes!!! 只用一個findOne()類方法便可獲得所有的posts的數據。

執行exec()來run:

const Post = mongoose.model("Post", postSchema)
const User = mongoose.model('User', userSchema)
//添加一些數據,並存入MongoDB數據庫
User.findOne({name:
/azat/i}) .populate('posts') .exec((err, user) => { if (err) return handleError(err) console.log('The user has % post(s)', user.posts.length) })

⚠️ ObjectIdNumberString, and Buffer are valid data types to use as references,

meaning they will work as foreign keys in the relational DB terminology.

知識點: 

  • 正則表達式:找到全部匹配azat的string,大小寫敏感, case-insensitively。
  • console.log中的 %, 一種字符串插入符號的寫法,把user.posts.length插入這個字符串。

 

也能夠只返回一部分填入的結果。例如,咱們可以限制posts的數量爲10個:

⚠️在mongoose, path指 定義一個Schema中的type類型的名字

.populate({
  path: 'posts',
  options: { limit: 10, sort: 'title'}
})

 

有時候,只會返回指定的fileds,而不是整個document,使用select:

 .populate({
      path: 'posts',
      select: 'title',
      options: { 
        limit: 10, 
        sort: 'title' 
      }
    })

 

另外,經過一個query來過濾填入的結果!

.populate({
  path: 'posts',
  select: '_id title text',
  match: {text: /node\.js/i},
  options: { limit: 10, sort: '_id'}
})

 

查詢選擇的屬性使用select, 值是一個字符串,用空格分開每一個field name。

建議只查詢和填入須要的fields,由於這樣能夠防止敏感信息的泄漏leakage,下降系統風險。

 

populate方法能夠find()鏈接使用,即多個document的查詢。

 

問題:

1. user.posts.length,這是user.posts是一個數組嗎?因此可使用length方法。

答:是的,在定義userSchema時,posts field的數據類型是數組。

 

2.exec()的使用:

Model.find()返回<Query>, 而後使用Query.populate()並返回<Query>this, 而後使用Query.exec()返回Promise

 

3 type和ref

type表明SchemType。ref屬性是SchemaType Options的一種。和type屬性配合使用。

 

4.上面的案例,如何保存有關聯的數據?

 

var user = new User({name: "John", _id: 2})
var post = new Post({title: "New land", text: "Hello World!"})
user.posts = post._id
post._creator = user._id
user.save() post.save()

User.findOne({_id: 2}).populate("posts")
  .exec((error, user) => {
    console.log(user.posts.length)
  })

 

 

 

還須要看官網的Populate一章。講真,這本書講的都很淺顯,有的沒有說清楚。

 

理解:User和Post各自有一個含有選項ref的path。所以雙方創建了關聯。

 

 


 

 

官方guide Populate()

 

Population是指: 在一個document內,用來自其餘collection(s)的document,自動地取代指定paths的值。

咱們能夠populate一個單獨的document,多個documents, 普通的object,多個普通的objects, 或者從一個query返回的全部objects。

 

基礎

const mongoose = require('mongoose')
const Schema = mongoose.Schema
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})
// var db = mongoose.connection

const personScheam = Schema({
  _id: Schema.Types.ObjectId,
  name: String
  age: Number,
  stories: [{ type: Schema.Types.ObjectId, ref: "Story"}]
})

const storySchema = Schema({
  author: { type: Schema.Types.ObjectId, ref: "Person"},
  title: String,
  fans: [{ type: Schema.Types.ObjectId, ref: "Person"}]
})

const Story = mongoose.model("Story", storySchema)
const Person = mongoose.model("Person", personScheam)

 

注意⚠️

  • 使用ref選項的path的類型必須是ObjectId, Number, String, Buffer之一。
  • 一般使用ObjectId, 除非你是一個高級用戶或有充分如此作的緣由 

 

saving refs

保存refs到其餘documents和你保存屬性的方式同樣,指須要分配_id值:

const author = new Person({
  _id: new mongoose.Types.ObjectId,
  name: "Ian Fleming",
  age: 50
})

author.save((err) => {
  if (err) return handleError(err)

  const story1 = new Story({
    title: "Casino Royale",
    author: author._id
  })

  story1.save((err, story1) => {
    if (err) return handleError(err)
    console.log("Success stores", story1.title)
  })
})

 

上面的代碼,由於story1有外鍵author(即經過_id創建了兩個documents的關聯), 因此story1能直接populate author的數據。

 

Population

如今填入story的author,使用query builder:

Story.findOne({ title: "Casino Royale"})
  .populate('author')
  .exec((err, story) => {
    if (err) return handleError(err)
    console.log("The author is %s", story.author.name)
  })

 

經過在返回結果前運行一個獨立的query,(findOne()方法返回的是一個Query對象)

填入的paths再也不是它們的原始的_id, 它們的值被替換爲從數據庫返回的document。

Arrays of refs和 非Arrays of refs的工做方式同樣。都是在query對象上調用populate方法,並返回一個array of documents來替代原始的_ids

 

Setting Populated Fields

也能夠手動填入一個對象,來替換_id。把一個document對象賦值給author屬性。

這個對象必須是你的ref選項所涉及的model的一個實例:

//假設以前已經向數據庫存入了一個person和一個story, story有person的外鍵:
Story.findOne({ title: "Casino Royale"}, (error, story) => {
  if (error) {
    return handleError(error)
  }
 Person.findOne({name: "Ian Fleming"}).exec((err, person) => {
    story.author = person
console.log(story.author.name) })
})
//控制檯會輸出author的名字

 

這是不使用populate的方法。和使用populate的效果同樣,都是替換掉了_id。

 

hat If There's No Foreign Document?

Mongoose populate不像傳統的SQL joins。相似left join in SQL。

Person.deleteMany({ name: "Ian Fleming" }, (err, result) => {
  if (err) {
    console.log("err: ",err)
  } else {
    console.log("res: ", result)
  }
});

//由於沒有了Person中的document, story.author.name是null。
Story.findOne({ title: "Casino Royale"})
  .populate('author')
  .exec((err, story) => {
    if (err) return handleError(err)
    console.log("The author is %s", story.author.name)
  })

 

若是storySchema的authors path是數組形式的, 則populate()會返回一個空的array

 

Field Selection

若是隻想從返回的populated documents獲得指定的fields, 能夠向populate()傳入第二個參數: field name\

populate(path, [select])

Story.findOne({ title: "Casino Royale"})
  .populate('author', 'name')
  .exec((err, story) => {
    if (err) return handleError(err)
    console.log("The author is %s", story.author.name)
    //返回The authors age is undefined
    console.log('The authors age is %s', story.author.age)  
  })

 

 

Populating Multiple Paths

若是咱們想要同時填入多個paths, 把populate方法連起來:

Story.
  find(...).
 populate('fans').
  populate('author').
  exec();

 

 

Query conditions and other options

若是咱們想要填入populate的fans數組基於他們的age, 同時只選擇他們的名字,並返回最多5個fans, 怎麼作?

Story.find(...)
  .populate({
    path: 'fans',
    match: {age: { $gte: 21 }},
    // 使用"-_id",明確表示不包括"_id"field。
    select: "name -_id",
    options: { limit: 5}
  })
  .exec()

 

 

Refs to chlidren

本章Populate官網教程提供的案例,auhtor對象的stories field並無被設置外鍵。

所以不能使用author.stories獲得stories的列表。

 

這裏有2個觀點:perspectives:

第一, 你想要author對象知道哪些stories 是他的。一般,你的schema應該解決one-to-many關係,經過在many端加一個父pointer指針。可是,若是你有好的緣由想要一個數組的child指針,你可使用push()方法,把documents推到這個數組上:

author.stories.push(story1)
author.save(callback)

 

這樣,咱們就能夠執行一個find和populate的聯合

 Person.
  findOne({ name: 'Ian Fleming' }).
  populate('stories'). // only works if we pushed refs to children
  exec(function (err, person) {
    if (err) return handleError(err);
    console.log(person);
  });

 

是否真的要設置2個方向的pointers是一個可爭論的地方。

 

第二,做爲代替, 咱們能夠忽略populating,並直接使用find()方法,找到stories:

Story.
  find({ author: author._id }).
  exec(function (err, stories) {
    if (err) return handleError(err);
    console.log('The stories are an array: ', stories);
  });

 

Populating an existing document

若是咱們有一個正存在的mongoose document並想要填入一些它的paths,

可使用document#populate() , 返回Document this。

doc.populate(path|options, callback)
// or
doc.populate(options).execPopulate()

 

Populating multiple existing documents

若是咱們有多個documents或者plain objects, 咱們想要填入他們,使用Model.populate()方法。

這和document#populate(), query#populate()方式相似。 

populate(docs, options, [callback(err, doc)])  返回Promise.

  • docs <Document|Array>,一個單獨的對象或者一個數組的對象。
  • options <Object| Array> 一個hash的key/value對兒。可以使用的頂級options:
    • path: 值是要填入的path的名字
    • select: 選擇要從數據庫獲得的fields
    • match: 可選的查詢條件用於匹配
    • model: 可選的model的名字,用於填入。(已知是用在不一樣數據庫的model實例的填入)
    • options: 可選的查詢條件,好比like, limit等等。
    • justOne: 可選的boolean,若是是true,則設置path爲一個數組array。默認根據scheam推斷。
// populates an array of objects
// find()返回一個query,裏面的result是一個array of documents, 所以opts也應該是一個array of document
User.find(match, function (err, users) { var opts = [{ path: 'company', match: { x: 1 }, select: 'name' }] var promise = User.populate(users, opts); promise.then(console.log).end(); })

填入一個object, 和上面填入一個array of objects, 和填入不少plain objects。具體見文檔

 

Populating across multiple levels跨越多層的填入

一個model的內的實例能夠互相關聯。即Self Joins

(這在Rails中的例子也是自身model上加一個foreign_key)

一個user schema能夠跟蹤user的朋友:

⚠️,關鍵使用ref選項,引用"User"自身!!!

var userSchema = new Schema({
  name: String,
  friends: [{ type: Scheam.Types.ObjectId, ref: 'User'}]
})

Populate讓你獲得一個user的朋友的列表。

可是若是你也想要一個user的朋友的朋友哪?加一個populate選項的嵌套: 

User.
  findOne({ name: 'Val' }).
  populate({
    path: 'friends',
    // Get friends of friends - populate the 'friends' array for every friend
    populate: { path: 'friends' }
  });

一個完整的例子:

//populate.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})

const userSchema = new Schema({
  _id: Number,
  name: String,
  friends: [{
    type: Number,
    ref: 'User'
  }]
})

const User = mongoose.model("User", userSchema)

//存入下面的數據
 var user = new User({ name: "chen", _id: 3, friends: [4]  }).save()
 var user2 = new User({ name: "haha", _id: 4, friends: [3, 5] }).save()
 var user3 = new User({ name: "ming", _id: 5, friends: [5] }).save()

 

執行查詢,使用populate選項:

User.findOne({_id: 3})
  .populate({
    path: 'friends',
    populate: {path: 'friends'}
  })
  .exec((err, result) => {
    console.log(result)
  })
//返回
{ posts: [],
  friends:
   [ { posts: [],
       friends:
        [ { posts: [], friends: [ 4 ], _id: 3, name: 'chen', __v: 0 },
          { posts: [], friends: [ 5 ], _id: 5, name: 'ming', __v: 0 } ],
       _id: 4,
       name: 'haha',
       __v: 0 } ],
  _id: 3,
  name: 'chen',
  __v: 0 }

 

 

Populating across Databases跨越數據庫的填入

使用model選項

以前的練習:

//引進mongoose
const mongoose = require('mongoose')
//獲得Schema構建器
const Schema = mongoose.Schema
//mongoose實例鏈接到本地端口27017的數據庫test
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})
//獲得connection對象實例, 由於實際的緣由,一個Connection等於一個Db
var db = mongoose.connection
// with mongodb:// URI, 建立一個Connection實例
// 這個connection對象用於建立和檢索models。
// Models老是在一個單一的connection中使用(scoped)。
var db = mongoose.createConnection('mongodb://user:pass@localhost:port/database'); 

假如,events和conversations這2個collection儲存在不一樣的MongoDB instances內。

var eventSchema = new Schema({
  name: String,
  // The id of the corresponding conversation
  //  ⚠️沒有使用ref
  conversation: Schema.Typs.ObjectId
});
var conversationSchema = new Schema({
  numMessages: Number
});
var db1 = mongoose.createConnection('localhost:27000/db1');
var db2 = mongoose.createConnection('localhost:27001/db2');
//⚠️,個人電腦上不能同時開2個mongd,提示❌
exception in initAndListen: DBPathInUse: Unable to lock the lock file: /data/db/mongod.lock (Resource temporarily unavailable). Another mongod instance is already running on the /data/db directory, terminating
var Event = db1.model('Event', eventSchema); var Conversation = db2.model('Conversation', conversationSchema);

這種狀況下,不能正常使用populate()來填入數據,須要告訴populate使用的是哪一個model:

Event.
  find().
  populate({ path: 'conversation', model: Conversation }).
  exec(function(error, docs) { /* ... */ });

 

實踐的例子: 跨MongoDB databases實例。

// Populating across Databases
const mongoose = require('mongoose')
const Schema = mongoose.Schema
mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true })
var db2 = mongoose.createConnection('mongodb://localhost:27017/db2', { useNewUrlParser: true }) // 建立2個Schema。
var eventSchema = new Schema({
  name: String,
  conversation: Schema.Types.ObjectId
});
var conversationSchema = new Schema({
  numMessages: Number
});

// 在test 數據庫上建立一個Event類的實例。
var Event = mongoose.model('Event', eventSchema) var event = new Event({name: "click"}).save() // 在db2 數據庫上建立一個Conversation類的實例 var Conversation = db2.model('Conversation', conversationSchema); var conversation = new Conversation({numMessages: 50}).save()
// 我在mongDb shell中給event document增長了一個field(conversation: XX),值是conversation實例的_id

 

啓動上面的腳本後,我修改腳本去掉建立實例的2行代碼,而後添加一個find和populate, 而後重啓腳本:

 

Event.find()
  .populate({ path: 'conversation', model: Conversation})
  .exec((error, docs) => {
    console.log(docs)
  })

 

 

 

成功,填入conversation: (這個例子就是在不一樣database的一對一關聯)

 

[ { _id: 5c4ad1f2916c8325ae15a6ac,
    name: 'click',
    __v: 0,
    conversation: { _id: 5c4ad1f2916c8325ae15a6ad, numMessages: 50, __v: 0 } } ]

 

 

 

上面的練習,

  • 若是在find()內去掉model,再次運行腳本,返回的數組內的conversation field的值是 null
  • 若是在find()內去掉model, 而後在eventSchema內加上ref,再次運行腳本。返回null。

 

上面的練習,把2個model放在同database下,能夠正確運行的✅。

即eventSchema沒有使用 ref, 但在find().populate()內使用了model: "Conversation", 能夠填入對應的conversation實例。

由於官方文檔:Query.prototype.populate()的參數[model]的解釋是這樣的:

«Model» The model you wish to use for population. 
If not specified, populate will look up the model by the name in the Schema's ref field.

即,

若是populate方法內指定了model選項,則從這個model中找對應的document。

若是沒有指定model,纔會在eventSchema中找ref選項,由於ref的值就是一個model的名字。

結論就是,不管是model選項仍是 ref選項,它們都是把2個document鏈接起來的輔助。

 

Dynamic References via refPath

Populate Virtuals

Populate Virtuals: The Count Option

Populate in Middleware

 


 

 

Nested Documents

上一章population。 這是一種傳統的方法,來設計你的數據庫。它minic模仿了關係型數據庫設計並使用普通的forms和嚴格的數據原子化atomization。

 

The document storage model in NoSQL databases is well suited to use nested documents。

若是你指定最頻繁運行的查詢是什麼,使用nested documents是更好的選擇。

你能夠優化你的數據庫讓它傾向某一個查詢。

例如,大多數典型的使用案例是讀用戶數據。那麼代替使用2個collections(posts and users),

咱們能夠用單一的collections(users), 內部嵌套posts。

 

絕對使用哪一種方式更多的是建築學的問題,它的答案由具體使用決定。

例如,

  • 若是有相似blog的功能,多個用戶會讀取做者的posts,就須要獨立的查詢做者的posts。分開的collection會更好。
  • 若是posts只在做者的我的頁面使用,那麼最好就用nested documents。

 

使用Schema.Types.Mixed類型

const userSchema = new mongoose.Schema({
  name: String,
  posts: [mongoose.Schema.Types.Mixed]
})
// Attach methods, hooks, etc.
const User = mongoose.model('User', userSchema)

 

更靈活的Schema設計,分紅2個Schema:

const postSchema = new mongoose.Schema({
  title: String,
  text: String
})
// Attach methods, hooks, etc., to post schema
const userSchema = new mongoose.Schema({
  name: String,
 posts: [postSchema]
})
// Attach methods, hooks, etc., to user schema
const User = mongoose.model('User', userSchema)

 

增長子文檔到arrays:

由於使用了數組,因此可使用push, unshift, 等方法(在JavaScript/Node.js)或者MongoDB$push操做符號來更新user document:

User.updateOne(
  {_id: userId},
  {$push: {posts: newPost}},
  (error, results) => {
    // 處理錯誤和檢測結果
  }
)

 

操做符號有複雜的附加功能,能夠處理各類狀況

 

也可使用save():

var childSchema = new Schema({name: String})

var parentSchema = new Schema({
  children: [childSchema],
  name: String
})

var Parent = mongoose.model('Parent', parentSchema)

var parent = new Parent({
  children: [{name: 'Matt'}, {name: 'Sarah'}]
})
parent.children[0].name =  'Matthew'
parent.children.push({ name: 'Liesl'})
parent.save((error, result) => { if (error) return console.log(error) console.log(result) })

 

獲得:

{ _id: 5c47d630d93ce656805231f8,
  children:
   [ { _id: 5c47d630d93ce656805231fa, name: 'Matthew' },
     { _id: 5c47d630d93ce656805231f9, name: 'Sarah' }  ,
{ _id: 5c47d9b07517b756fb125221, name: 'Liesl' } ], __v:
0 }

注意⚠️,新增了3個child,  和parent一塊兒存在mongoDB的test數據庫的parents collections內

 

查詢一個子document

每一個子document默認有一個_id

Mongoose document arrays有一個特別的id方法用於搜索一個doucment array來找到一個有給定_id值的document。

var doc = parent.children.id(_id)

 

移除使用remove方法,至關於在子文檔內使用.pull() 

parent.children.pull(_id)
//等同
parent.children.id(_id).remove()

//對於:a single nested subdocument: parent.child.remove()
//等同 parent.child = null

 

 


官方文檔Queries

 

Mongoose models提供用於CRUD操做的靜態幫助函數。這些函數返回一個mongoose Query 對象。

  • Model.deleteOne(),  deleteMany()
  • Model.find()
  • Model.findById(), 及衍生出findByIdAndDelete(),  findByIdAndRemove, findByIdAndUpdate
  • Model.findOne(),  及衍生出findOneAndDelete(), findOneAndRemove, findOneAndUpdate
  • Model.replace() ,相似update(), 用傳入的doc取代原來的document
  • Model.updateOne(),  Model.updateMany()。

一個Query對象可使用.then()函數。

 

query with callback

當使用一個query並傳入一個callback(), 你指定你的query做爲一個JSON document。

這個JSON document的語法和MongoDB shell相同。

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

// find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
Person.findOne({ 'name.last': 'Ghost' }, 'name occupation', function (err, person) {
  if (err) return handleError(err);
  console.log('%s %s is a %s.', person.name.first, person.name.last,
    person.occupation);
});

 

⚠️在Mongoose內,全部的callback都使用這個模式callback(error, result)

  • 若是有error存在,則error參數包含a error document。 result的值是null
  • 若是query是成功的,error參數是null, 而且result被填入populated查詢的結果。

 

findOne()的例子:

Adventure.findOne({ type: 'iphone' }, function (err, adventure) {});
// same as above
Adventure.findOne({ type: 'iphone' }).exec(function (err, adventure) {});

 

// specify options, in this case lean
Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }, callback);

// same as above
Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }).exec(callback);

// chaining findOne queries (same as above)
Adventure.findOne({ type: 'iphone' }).select('name').lean().exec(callback);

 

lean選項爲true,從queries返回的documents是普通的javaScript 對象,而不是MongooseDocuments。

 

countDocuments()的例子

在一個collection中,計算符合filter的documents的數量.

 

query but no callback is passed

一個Query 可讓你使用chaining syntax,而不是specifying a JSON object

例子:

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)
//等同於使用query builder:
Person.
  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);

 

 

Queries不是promises

可使用.then函數,   可是調用query的then()可以執行這個query屢次。

const q = MyModel.updateMany({}, { isDeleted: true }, function() {
  console.log('Update 1');
});

q.then(() => console.log('Update 2'));
q.then(() => console.log('Update 3'));

 

上個例子,執行了3次updateMany()。

  • 第一次使用了callback。
  • 後2次,使用了then()。

注意⚠️不要在query混合使用回調函數和promises。

 


  

Virtual Fields (Virtuals)

不存在於數據庫,可是像regular field in a mongoose document。就是mock,fake。 

Virtual fields的用途:

  • dynamic data
  • creating aggregate fields

例子,一個personSchema,有firstName, lastName2個fields,和一個Virtual fields(fullName),這個Virtual fullName無需真實存在。

 

另外一個例子,兼容之前的database。每次有一個新的schema, 只需增長一個virtual來支持舊的documents。

例如, 咱們有上千個用戶記錄在數據庫collection,咱們想要開始收集他們的位置。所以有2個方法:

1. 運行一個migration script,爲全部old user documents增長一個location field, 值是none。

2. 使用virtual field 並在運行時,apply defaults。

 

再舉一個例,加入有一個大的document,咱們須要獲得這個document的部分數據,而不是全部的數據,就可使用virtual field來篩選要顯示的數據:

//從Schema中篩選出一些fields,放入虛擬field"info"
userSchema.virtual('info')
  .get(function() {
    return {
      service: this.service,
      username: this.username,
      date: this.date,
      // ...
    }
  })

 

定義a virtual :

  1. personSchema.virtual('fullName')建立一個virtual type。
  2. 使用a getter function, get(fn), 返回<VirtualType>this。 (不要使用箭頭函數, this是一個instance/document)

完整的例子:

const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/myproject', {useNewUrlParser: true})

var personSchema = new mongoose.Schema({
  name: {
    first: String,
    last: String
  }
})

//定義一個virtualType personSchema.virtual(
'fullName').get(function () { return this.name.first + ' ' + this.name.last; }); var Person = mongoose.model('Person', personSchema) // var axl = new Person({ // name: { // first: 'Axl', // last: 'Rose' // } // }).save((error, result) => { // if (error) return console.log(error) // console.log(result) // }) Person.findOne({"name.first": 'Axl'}, (error, result) => { console.log(result.fullName) })

上面的例子使用了Schema#virtual()方法。定義了一個虛擬field,並VirtualType#get()方法定義了一個getter。天然也能夠定義一個setter,使用set()方法:(關於get,set見👇一章)

//爲virtual field 「fullName」添加了寫入write的set()函數
personSchema.virtual('fullName').set(function(v) {
  var parts = v.split(" ")
  this.name.first = parts[0]
  this.name.last = parts[1]
})

//把一個名字字符串存入到fullName filed。本質是存到了name.first和name.last
Person.findOne({"name.first": 'Axl'}, (error, person) => {
  person.fullName = "chen hao"
  person.save()
}) 

結論: get和set方法就是read/write的存取方法。

⚠️: 由於virtuals不能儲存在MongoDB,因此不能查詢他們query。

 

toJSON和toObject(方法/構建器選項)對Virtuals fields的影響

默認,這2個方法不會影響到虛擬fileds。能夠經過傳{virtuals: true}來讓這2個方法對virtual fields生效。

例子:

由於使用選項{getters: true}, 全部getters都會使用,包括virtual getters和path getters

 

path getters指對Schema對象中的path(其實就是fields的另類稱呼)設置一個get(callback)函數:

const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/myproject', {useNewUrlParser: true})

var schema = new mongoose.Schema({ name: String });
schema.path('name').get(function (v) {
  return v + ' is my name';
});
schema.set('toObject', { getters: true }); var People = mongoose.model('People', schema);
var m = new People({ name: 'Max Headroom' });
console.log(m)

 

Document.propotype.toObject()

把document轉化爲一個普通的javaScript object, 並準備儲存到數據庫。返回一個js object。

toObject()方法的本質就是對document的一些設定,根據這些設定,把數據轉化爲js object.

參數只有[options], 有7個選項:

  • {getters: false} 默認是false,若是true,使用全部的getters。
  • {minimize: true} 默認是true, 即忽略輸出中的任何空對象。假如這個document, 有一個field的值是null,則這個filed不會轉化爲js object的一部分。
  • [options.versionKey=true] 默認是true, 即生成的js object形式的document內包含一個_v 屬性
  • 其餘的4具體見文檔

toObject使用的地方:

1. Schema構建器的選項

model對象自己是schema對象的副本。

而Schema()構建器的options內包括toJson和toObject選項,默認狀況下構建器不會使用這2個選項

因此若是你在Schema構建器上使用toObject選項(如上面的例子),則生成的doc必然使用toObject選項設置的規則,其中minimize和versionKey是默認true。

2. 對某個document使用.

 


 

Schema Type Behavior Amendment

Schema不是靜態的類型定義。 

Mongoose可讓開發者在Schema內,定義/寫入 getters(get), setters(set)和defaults(default) 

 

get是在一個field被讀時引用。 set是當一個field被賦值時引用。

開發者經過他們能夠修改實際的database document的值。

Mongoose有4個方法:set()get()default() and validate()

  • set(): 例如把一個string所有小寫化, 當這個value被分配時。
  • get(): 增長一個"thousands"符號給一個數字,當這個number被提取時。
  • default(): 生成一個新的ObjectId
  • validate(): 用於檢測email pattern; 在save()前被激活。

利用上面的4個方法,咱們能夠在Mongoose Schema的fields中定義(和type在同一層:)

postSchema = new mongoose.Schema({
  slug: { 
    type: String, 
    set: function(slug) { 
      return slug.toLowerCase()
    }
  },
  numberOfLikes: {
    type: Number,
    get: function(value) {
      return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    }
  },  
  authorId: { 
    type: ObjectId, 
    default: function() { 
      return new mongoose.Types.ObjectId() 
    } 
  },
  email: { 
    type: String, 
    unique: true, 
    validate: [ 
      function(email) {
        return (email.match(/[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i) != null)}, 
      'Invalid email'
    ] 
  }
})

 

由於會根據需求動態的定義Schema,因此Mongoose提供了另外的方法來修補Schema behavior:

chain methods--這須要2步:

  1. 使用Schema.path(name)來獲得SchemaType。
  2. 使用Schema.get(fn)來設置getter方法。

⚠️和定義virtual fields的方式相似。

SchemaType.get(fn), 返回<this>。  爲這個schema type的全部實例附加一個getter。

例如單獨的爲numberOfPosts field建立一個getter方法:

userSchema
  .path('numberOfPosts')
  .get(function() {
    return this.posts.length
  })

 

提示:

什麼是path?

path就是一個名字,特指Schema內的嵌套的field name和它的父對象。

例如, 咱們有ZIP代碼(zip)做爲contact.address的一個child,

好比user.contact.address.zip, 而後contact.address.zip就是一個path。


 

官方文檔Validation 

mongoose提供了validation功能,如下是幾條驗證的rules:

  1.  Validation定義在SchemaType內。和type同層。
  2.  Validation是一個middleware。默認Mongoose註冊它,做爲每一個schema的pre('save')鉤子。
  3.  你能夠手動地運行validation,使用doc.validate(callback)或者doc.validateSync()
  4.  Validators 不會在未定義values的field上進行驗證。可是,惟一一個例外是required驗證器
  5.  Validation是一種異步的遞歸;當你調用Model#save, 子doc驗證也會被執行。若是發生一個error, 你的Model#save回調會接收它。
  6. Validation能夠客製化。

例子:

這個例子由於生成的cat實例的name沒有賦值,在save時,沒法經過validation。

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const { expect } = require('chai')

mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})

var schema = new Schema({
  name: {
    type: String,
    required: true
  }
})

var Cat = mongoose.model("Cat", schema)

var cat = new Cat()
cat.save((err) => {
  console.log(err)
  expect(err.errors['name'].message).to.equal("Path `name` is required.")
})

 

err是一個javascript object:

經過err.errors.name.message獲得 'Path `name` is required.'

{ ValidationError: Cat validation failed: name: Path `name` is required.
    at ValidationError.inspect (/Users/chen /node_practice/driver/node_modules/mongoose/lib/error/validation.js:59:24)
    at formatValue (internal/util/inspect.js:523:31)
    //...一大坨路徑
  errors:
   { name:
      { ValidatorError: Path `name` is required.
          at new ValidatorError (/Users/chentianwei/node_practice/driver/node_modules/mongoose/lib/error/validator.js:29:11)
          at validate (/Users/chentianwei/node_practice/driver/node_modules/mongoose/lib/schematype.js:926:13)
 //...一大坨路徑
        message: 'Path `name` is required.',
        name: 'ValidatorError',
        properties:
         { validator: [Function],
           message: 'Path `name` is required.',
           type: 'required',
           path: 'name',
           value: undefined },
        kind: 'required',
        path: 'name',
        value: undefined,
        reason: undefined,
        [Symbol(mongoose:validatorError)]: true } },
  _message: 'Cat validation failed',
  name: 'ValidationError' }

 

SchemaType.required()驗證器

參數

第一個參數:required能夠是《Boolean| Function | Object》

第二個參數:[message],string, 提供錯誤信息。

 

客製化驗證器

SchemaType#validate()

客製化的驗證器也能夠是異步的。讓validator 函數返回一個promise(或者使用async function), mongoose會等待promise去處理。

也可使用回調函數做爲參數。

(具體案例見文檔)

 

Validations Errors

若是驗證失敗,會返回錯誤的信息,其中包括一個errors對象。這個errors對象內部包含ValidatorError對象,kind, path, value, message, reason屬性。

若是你在驗證器內使用throw new Error(//...), 若是產生❌,reason屬性會包含這個❌的信息。

見上面的☝️代碼塊。

案例 

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const { expect } = require('chai')

mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true})

var toySchema = new Schema({
  color: String,
  name: String
})
//給color設置驗證函數,color的值指定在red, white, gold內選擇。
var validator = function(value) {
  return /red|white|gold/i.test(value)
}
toySchema.path('color').validate(validator, 'Color `{VALUE}` not valid', 'Invalid color')
// 給name設置驗證函數,若是傳入的值不能經過判斷,就拋出一個Error對象。並提供錯誤的信息。
toySchema.path('name').validate(function(v) {
  if (v !== 'Turbo Man') {
    throw new Error('Need to get a Turbo Man for Christams')
  }
  return true
}, 'Name `{VALUE}` is not valid')

// 聲明Toy類
var Toy = mongoose.model('Toy', toySchema)
// 實例化toy
var toy = new Toy({color: 'Green', name: 'Power Ranger'})
// 保存,回調函數內進行測試。
toy.save(function(err) {
  console.log(err.errors.color)
  expect(err.errors.color.message).to.equal('Color `Green` not valid')
  expect(err.errors.color.path).to.equal('color')
  // 若是驗證器throw an error, "reason"屬性會包括錯誤的信息及stack trace
  console.log(err.errors.name)
  expect(err.errors.name.message).to.equal('Need to get a Turbo Man for Christams')
  expect(err.errors.name.value).to.equal('Power Ranger')
})

 

 

Update Validations驗證

默認狀況下,更新驗證不會使用,須要指定runValidators選項。

在doc更新時,激活驗證。如 updateOne(), findOneAndUpdate()等。

var opts = { runValidators: true };
Toy.updateOne({}, { color: 'bacon' }, opts, function (err) {
  assert.equal(err.errors.color.message,
    'Invalid color');
});

 

 


 

官方文檔Middleware

Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions。

 

中間件被指定在schema層,寫plugins時很是有用!

 

Mongoose 有4種中間件:document, model, aggregate, query。

 

document中間件被如下函數支持:

  • validate
  • save
  • remove
  • init

Query中間件被如下Model和Query函數支持:

  • count
  • deleteMany, deleteOne
  • find系列,findOne等
  • update, updateOne, updateMany

Model中間件被如下model 函數支持:

  • insertMany

 

Pre

Pre middleware functions are executed one after another, when each middleware calls next.

當pre中間件調用next函數後,就會執行後續的pre中間件。

var schema = new Schema(..)
schema.pre('save', function(next) {
  // do stuff
  next()
})

 

代替使用next,可使用一個函數返回一個promise,也能夠用async/await

 

關於return的使用

調用next()不會中止中間件函數內的後續代碼的執行。除非使用return next()

var schema = new Schema(..);
schema.pre('save', function(next) {
  if (foo()) {
    console.log('calling next!');
    // `return next();` will make sure the rest of this function doesn't run
    next();
  }
  // 若是用return next(),下面的代碼就不會執行了。
  console.log('after next');
});

 

 

Use Cases使用案例

中間件用於細化邏輯。下面是一些其餘的點子:

  • 複雜的驗證
  • 移除依賴的文檔
  • asynchronous defaults
  • asynchronous tasks that a certain action triggers

 

錯誤處理

若是pre hook發送❌,以後的中間件或者勾住的函數不會執行。做爲代替,Mongoose會傳遞一個error給回調函數, and/or拒絕返回的promise。

schema.pre('save', function(next) {
  const err = new Error('something went wrong');
  // If you call `next()` with an argument, that argument is assumed to be
  // an error.
  next(err);
});

// later...

// Changes will not be persisted to MongoDB because a pre hook errored out
myDoc.save(function(err) {
  console.log(err.message); // something went wrong
});

 

 

Post 中間件

在能夠hooked method和它的pre中間件完成後,post中間件被執行。

schema.post('init', function(doc) {
  console.log('%s has been initialized from the db', doc._id);
});

 

 

異步的Post Hooks

若是你的post hook function接受至少2個參數, mongoose會假設第2個參數是一個next()函數,你調用它來激活下一個中間件。

schema.post('save', function(doc, next) {
  setTimeout(function() {
    console.log('post1')
    next();  //執行下一個post hook
  }, 10)
})

// 只有第一個post中間件調用next(), 纔會執行下一個post中間件
schema.post('save', function(doc, next) {
  console.log('post2');
  next();
});

 

 

注意⚠️: mongoose有內建的pre('save')鉤子會調用validate()函數。

一句話就是pre('validate')和post('validate')會在pre('save')鉤子以前被調用。

 


 

Express.js + Mongoose = True MVC

爲了不重建全部其餘的和ODM不相關的部分,如template, routes等等,根據上一章的Blog進行重構。

使用Mongoose。

在MongoDB和請求handlers之間產生一個抽象層。

 

代碼參考:https://github.com/azat-co/blog-express/tree/mongoose

安裝:

$npm install mongoose

 

 

修改app.js:

由於mongoose是模仿關係型數據庫的一個關係型的數據模型,咱們須要在主程序文件夾創建一個models文件夾,用於儲存數據結構和關係。

而後在app.js內加上

const mongoose = require('mongoose')
const models = require('./models')

 

而後

創建鏈接聲明,由於Mongoose使用models,不會直接使用Mongodb的collections因此去掉:

const collections = {
  articles: db.collection('articles'),
  users: db.collection('users') }
加上鍊接:
const db = mongoose.connect(dbUrl, {useMongoClient: true})

 

修改代碼:

經過req對象獲得mongoose models, 而後就能在每一個Express.js route內操做MongoDb的數據.

app.use((req, res, next) => { if (!models.Article || !models.User) { // <--- ADD
    return next(new Error('No models.')) // <--- UPDATE
 } req.models = models // <--- ADD
  return next() })

OK了,從mongoskin到Mongoose完成。

 

下一步,創建shcmeas,並export。

爲了說明代碼複用,咱們從routes/article.js抽出find方法到models/article.js。

全部的database methods都這麼作。

//GET /api/articles API route
exports.list = (req, res, next) => {
  req.collections.articles.find({}).toArray((error, articles) => {
    if (error) return next(error)
    res.send({articles: articles})
  })
}

抽出find,變成:

articleSchema.static({
  list: function(callback) {
//第2個參數null是表示不設置projection
this.find({}, null, {sort: {_id: -1}}, callback) } })

而後編譯schema and methods 進入a model。

module.exports = mongoose.model('Article', articleSchema)

完整的article.js代碼:

const mongoose = require('mongoose')

const articleSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,  //一個驗證器,不能爲空
    // validate驗證器,第2個參數是message
    validate: [function(value){
      return value.length <= 120
    }, 'Title is too long(120 max)'],
    default: 'New Post' //默認值
  },
  text: String,
  published: {
    type: Boolean,
    default: false
  },
  slug: {
    type: String,
    // 對SchemaType的path(slug)的設置:
    set: function(value) {
      return value.toLowerCase().replace(' ', '-')
    }
  }
})

articleSchema。static({
  list: function(callback) {
    this.find({}, null, {sort: {_id: -1}}, callback)
  }
})

module.exports = mongoose.model('Article', articleSchema)

而後,

一樣增長user.js和index.js

隨着models下.js文件的增多,使用index.js來控制輸出models文件夾下的全部腳本文件,就變得很方便了。

exports.Article = require('./article')
exports.User = require('./user')

 

 

下一步修改routes 文件

一樣,把routes/article.js 的代碼中的Mongoskin collection改爲Mongoose models。

exports.show = (req, res, next) => {
  if (!req.params.slug) return next(new Error('No article slug.'))
  // req.collections.articles.findOne({slug: req.params.slug}, (error, article) => {
  //使用Mongoose models:
  req.models.Article.findOne({slug: req.params.slug}, (error, article) => {
    if (error) return next(error)
    if (!article.published  && !req.session.admin) return res.status(401).send()
    res.render('article', article)
  })
}

 

這個show函數,用於顯示一個具體的article詳細內容的頁面。

// 進口routes文件夾內的全部腳本的輸出。
const routes = require('./routes/index.js')

app.get('/articles/:slug', routes.article.show)

 

⚠️提示:

這裏的next()函數是。express實例app的方法use中的callback內的參數。

app.use([path], callback[, callback...])

回調callback能夠屢次的調用。爲了方便,使用next函數,表明完成這個回調,並進行下一個callback。

 

routes/article.js至關於Rails中的controller,

app.js中的app.get(url, callback)至關於Rails中的routes內的一條route.

 

思考:

express實例app是一根實例。

  • 儲存了一大堆中間件的實例方法:app.use()
  • 它存儲了Mongoose的Schema->Models
  • 處理服務器路徑routes, 使用app.get,app.post等方法來操做數據庫。

route->control方法->model(MongoDB)

 

以後還要改list方法, add,  edit,  del, postArticle, admin方法。具體見代碼:

(完整代碼:https://github.com/azat-co/blog-express/blob/mongoose/routes/article.js)

關於express.edit。

有方便的語法糖findByIdAndUpdate。可是有些hook不會被激活,須要肯定它的使用。

所以使用傳統的findById()加save()方法,能夠激活全部的hook, 更安全。

關於express.del

使用Model.deleteOne。

 

而後修改routes/index.js

exports.article = require('./article')
exports.user = require('./user')

exports.index = (req, res, next) => {
  req.models.Article.find({published: true}, null, {sort: {_id: -1}}, (error, articles) => {
    if (error) return next(error)
    res.render('index', {articles: articles})
  })
}
⚠️: null這個參數的位置自己是用於指定篩選出的fields,
若是不篩選則使用null,表示不進行篩選fields。全部的fields的數據都會取出。

 

最後,routes/user.js

具體還要約定第6章。

exports.authenticate = (req, res, next) => {
  if (!req.body.email || !req.body.password) { 
    return res.render('login', {error: 'Please enter your email and password.'}) 
  }
  req.models.User.findOne({
    email: req.body.email,
    password: req.body.password
  }, function (error, user) {
    if (error) return next(error)
    if (!user) return res.render('login', {error: 'Incorrect email&password combination.'})
    req.session.user = user
    req.session.admin = user.admin
    res.redirect('/admin')
  })
}

 

 

總結

本章探討使用Mongoose, 如何安裝,創建鏈接到數據庫。如何使用mongose schemas和SchemaType, 使用models, 如何使用它的query語法。如何使用hooks。 如何使用populate或nested documente。如何使用virtual fields.

並重構了Blog。使用Mongoose和一個MVC結構。

 

下章,講解創建RESTFUL APIs使用Express, Hapi(忽略)。 如今的趨勢是大前端加輕量後端。

這個趨勢讓開發團隊集中在最重要的方面:終端用戶--用戶的交互體驗。同時對商業來講,下降重複循環和更少的維護和開發費用。

測試驅動也很重要:使用Mocha, 它普遍用於Node.js測試。

相關文章
相關標籤/搜索