参考:博客 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见

Practical Node.js (2018版) 第7章:Boosting Node.js and Mongoose的更多相关文章

  1. Practical Node.js (2018版) 第5章:数据库 使用MongoDB和Mongoose,或者node.js的native驱动。

    Persistence with MongoDB and Mongoose https://github.com/azat-co/practicalnode/blob/master/chapter5/ ...

  2. Practical Node.js (2018版) 第10章:Getting Node.js Apps Production Ready

    Getting Node.js Apps Production Ready 部署程序需要知道的方面: Environment variables Express.js in production So ...

  3. Practical Node.js (2018版) 第9章: 使用WebSocket建立实时程序,原生的WebSocket使用介绍,Socket.IO的基本使用介绍。

    Real-Time Apps with WebSocket, Socket.IO, and DerbyJS 实时程序的使用变得越来越广泛,如传统的交易,游戏,社交,开发工具DevOps tools, ...

  4. Practical Node.js (2018版) 第3章:测试/Mocha.js, Chai.js, Expect.js

    TDD and BDD for Node.js with Mocha TDD测试驱动开发.自动测试代码. BDD: behavior-driven development行为驱动开发,基于TDD.一种 ...

  5. Practical Node.js (2018版) 第8章:Building Node.js REST API Servers

    Building Node.js REST API Servers with Express.js and Hapi Modern-day web developers use an architec ...

  6. Practical Node.js (2018版) 第4章: 模版引擎

    Template Engines: Pug and Handlebars 一个模版引擎是一个库或框架.它用一些rules/languages来解释data和渲染views. web app中,view ...

  7. Vue.js 学习笔记 第1章 初识Vue.js

    本篇目录: 1.1 Vue.js 是什么 1.2 如何使用Vue.js 本章主要介绍与Vue.js有关的一些概念与技术,并帮助你了解它们背后相关的工作原理. 通过对本章的学习,即使从未接触过Vue.j ...

  8. Node入门教程(7)第五章:node 模块化(下) npm与yarn详解

    Node的包管理器 JavaScript缺少包结构的定义,而CommonJS定义了一系列的规范.而NPM的出现则是为了在CommonJS规范的基础上,实现解决包的安装卸载,依赖管理,版本管理等问题. ...

  9. Node入门教程(6)第五章:node 模块化(上)模块化演进

    node 模块化 JS 诞生的时候,仅仅是为了实现网页表单的本地校验和简单的 dom 操作处理.所以并没有模块化的规范设计. 项目小的时候,我们可以通过命名空间.局部作用域.自执行函数等手段实现变量不 ...

随机推荐

  1. QML中打印

    1.console.log("123"); 2.console.log("a is ", a, "b is ", b); 3.打印代码块时间 ...

  2. What is event bubbling and capturing?

    What is event bubbling and capturing? 答案1 Event bubbling and capturing are two ways of event propaga ...

  3. 【resultType】Mybatis种insert或update的resultType问题

    Attribute "resultType" must be declared for element type "insert"或"update&q ...

  4. html 之 浮动(待补充)

    通过设置float 样式属性来决定区块标签的浮动 问题:区块与非区块浮动的特点(左右) 区块与区块浮动的特点(左右) ie浏览器与谷歌等浏览器的效果差异

  5. ComponentOne 2017 V1 发布

    在刚刚庆祝完Visual Studio20周年之后,我们迎来了ComponentOne 2017年第一个重要的版本. ComponentOne Studio与Visual Studio 2017配合发 ...

  6. springcloud问题随笔

    http://www.cnblogs.com/EasonJim/p/8085120.html 1.调用其它服务返回could not be queued for execution and no fa ...

  7. Java String 函数常用操作 & format() 格式化输出,代码详解

    package _String_; import java.util.*; import java.math.*; import java.lang.*; public class _Strings ...

  8. linux 进阶命令笔记(12月26日)

    1. df 指令 作用:查看磁盘空间 用法: #df -h       -h 表示以可读性较高的形式展示大小   2.free 指令 作用:查看内存使用情况 语法:#free -m       -m表 ...

  9. Nginx教程---01.Nginx入门

    create by 三七二十一 LZ参考视频(年代久远,但万变不离其宗): 链接:https://pan.baidu.com/s/1O_MmN0c3ckM6vbk08n8Qkg 密码:z9zr 01_ ...

  10. axios的学习与使用

    最近的项目都是使用的vue框架,所以请求都使用了vue官方推荐的axios. 官方中文介绍 此处记录一下常用的写法 执行 GET 请求 // 为给定 ID 的 user 创建请求 axios.get( ...