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

参考:博客 https://www.cnblogs.com/chentianwei/p/10268346.html

参考: mongoose官网(https://mongoosejs.com/docs/models.html)

参考: 英文:Boosting Node.js和MongoDB with Mongoose


 

 

简介:mongoose

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

ODM的概念对应sql的ORM,就是ruby on rails中的activerecord那因层。

activerecord包括migrations, Validations, associations, Query interface, 对应mvc框架中的Models。

ORM, Object-Relational Mappiing。

ODM的作用,定义数据库的数据格式schema, 然后通过它取数据,把数据库中的document映射成程序中的一个对象。这个对象有save, update的系列方法,有tilte, author等系列属性。

在调用这些方法时,odm会根据你调用时使用的条件,转化成mongoDb Shell语言,帮你发送出去。

自然,在程序内使用链式调用,比手写数据库语句更灵活也方便。

 

例子:

//先安装好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测试。

 


 

转载于:https://www.cnblogs.com/chentianwei/p/10297827.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值