MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
Mongoose是在node.js环境下对mongodb进行便捷操作的对象模型工具
快速上手
请先安装 MongoDB 和 Node.js。
下一步,npm
安装Mongoose:
1 | npm i mongoose |
连接数据库test
1 | const mongoose = require('mongoose'); |
connect()
返回一个状态待定(pending)的连接,接着我们加上成功和失败的提醒:
1 | const db = mongoose.connection; |
连接成功的时候,回调函数会被调用,假设下面所有函数都在这个回调函数里面。
Mongoose 里面一切始于 Schema。看一个简单的例子:
1 | const KittySchema = mongoose.Schema({ |
我们得到了一个带有 String
类型的 name
属性的 schema,接着把这个 schema 编译成一个 Model:
1 | const Kitten = mongoose.model('Kitten',kittySchema); |
model 是我们创造的 document 的 Class。在例子中,每个 document 都是一个实例,它的属性和行为都会被声明在 schema 。
1 | const felyne = new Kitten({name:'Felyne'}); |
加在 schema 的 methods
属性的函数会编译到 Model
的prototype,也会暴露搭配每个 document 实例:
1 | const a = new Kitten({name:'A'}); |
每个 document 会在调用它的 save 方法后保存到数据库中,注意回调函数的第一个参数永远是 error
1 | A.save(function(err,A){ |
如果有很多个实例,可以用下面的方法来获取 model 里面的所有数据
1 | Kitten.find(function(err,kittens){ |
模式(Schemas)
定义一个schema
Mongoose 的一切始于 Schema,每个 schama 都会映射到一个 MongoDB collection,并定义这个 collection 里面的文档的构成。
1 | const mongoose = require('mongoose'); |
document 里面的每个属性都会被转换成在 blogSchema 里面对应的 SchemaType。例如 title
属性会被转换成为 SchemaType String,而 date
属性会被转换成为 SchemaType Date。还可以像 meta
属性一样,更加详细地指定嵌套在里面的属性类型。
运行使用的 SchemaTypes 有:
- String
- Number
- Boolean
- Date
- Buffer
- Mixed
- ObjectId
- Array
Schema 的功能不只是定义文档结构和属性类型,还可以定义
- document 的 instance methods
- model 的 static Model methods
- 复合索引
- 文档的生命周期钩子,也称为中间件
创建一个 model
mongoose.model(modelName,schema)
函数把 schema 转换为一个 Model:
1 | const Blog = mongoose.model('Blob',blogSchema); |
实例方法(method)
document 是 Models 的实例。Document 有很多自带的实例方法,当然也可以自定义我们自己的方法
1 | // 定义一个 schema |
现在所有的 animal
实例都有 findSimilarTypes
方法:
1 | const Animal = mongoose.model('Animal',animalSchema); |
- 重写 mongoose 的默认方法会造成无法预料的结果
- 不要在自定义方法中使用 ES6 箭头函数,会造成 this 指向错误
静态方法(static)
添加 Model
的静态方法也十分简单,继续使用 animalSchema
举例:
1 | animalSchema.statics.findByName = function(name,cb){ |
同样注意的是不要在静态方法中使用 ES6 的箭头函数
查询助手(query helper)
查询助手作用于 query 实例,方便自定义扩展链式查询
1 | animalSchema.query.byName = function(name){ |
索引(index)
MongoDB 支持 secondary indexes。在 mongoose 中,我们在 Schema 定义索引。索引分字段级别和 schema 级别,复合索引需要在 schema 级别定义。
1 | const animalSchema = new Schema({ |
应用启动的时候,Mongoose 会自动调用 createIndex
初始化定义的索引。Mongoose 顺序处理每一个 createIndex,然后在 model 触发 ‘index’ 事件。在生产中建议禁止这种行为,因为创建索引会对性能造成重大影响。
1 | mongoose.connect('mongodb://user:pass@localhost:port/database',{autoIndex:false}); |
索引创建完成或者失败,Mongoose 会触发 index 事件
1 | // Will cause an error because mongodb has an _id index by default that is not sparse |
虚拟值(Virtual)
Virtual 是 document 的属性,但是不会被保存到 MongoDB.getter 可以用于格式化和组合字段数据,setter 可以很方便地分解一个值到多个字段。
1 | const personSchema = new Schema({ |
但是每次都要这样拼接就太麻烦了,使用 virtual property getter 这个方法允许定义一个 fullName 属性,但不必保存到数据库。
1 | personSchema.virtual('fullName').get(function(){ |
如果对 document 使用 toJSON()
或 toObject()
默认不包括虚拟值,需要额外向 toObject 或者 toJSON 传入参数 {virtuals:true}
也可以设定虚拟值的 setter,下面的例子中,当赋值到虚拟值的时候,会自动拆分到其他属性
1 | personSchema.virtual('fullName').get(function(){ |
需要注意的是,虚拟值不能用于查询和字段选择,因为虚拟值不存储在 MongoDB
别名(Alias)
Aliase 是一种特殊的虚拟值,它的getter 和 setter 会无缝链接到另一个值,这是一个节省带宽的做法,可以存储一个更短的属性名到数据库,同时在调用的时候保持可读性。
1 | const personSchema = new Schema({ |
选项
Schema 有很多可配置选项,可以在构造时传入或者直接 set
1 | new Schema({...},options); |
有效的选项:
- autoIndex
- bufferCommands
- capped
- collection
- id
- _id
- minimize
- read
- shardKey
- strict
- strictQuery
- toJSON
- toObject
- typekey
- validateBeforeSave
- versionKey
- skipVersioning
- timestamps
- useNestedStrict
option:autoIndex
应用启动的时候,Mongoose 自动发送 createIndex
指令,schema 里面的每个 index 都会被创建,如果需要关闭自动创建功能或者需要在创建后进行一系列操作,可以把 autoIndex
设为 false
,然后对 model 调用 ensureIndexes:
1 | const schema = new Schema({..},{autoIndex:false}); |
option:bufferCommands
默认情况下,mongoose buffers会在连接中断时发出命令,直到驱动程序重新连接为止。若要禁用缓冲,请将bufferCommands设置为false。
1 | const schema = new Schema({..},{bufferCommands:false}); |
schema 的bufferCommands 会重写全局的 bufferCommands 选项。
1 | mongoose.set('bufferCommands',true); |
option:capped
Mongoose 支持 MongoDB 的 capped collections ,要从底层把 collection 设定为 capped(封顶),可以把collection 的最大容量设定到 capped
选项(单位 bytes)
1 | new Schema({..},{capped:1024}); |
如果要传递多个选项,capped 也可以作为对象传递.这个试试需要显式传入必要值 size
1 | new Schema({..},{size:1024,max:1000,autoIndexId:true}) |
option:collection
Mongoose 通过 utils.toCollectionName 方法,默认生成 collection 的名称(生成 model 名称的复数形式)。设置这个选项可以自定义名称
1 | const dataSchema = new Schema({..},{collection:'data'}); |
option:id
Mongoose 会默认生成一个虚拟值 id
,指向文档的 _id
字段。如果你不需要 id
虚拟值,可以通过这个选项禁用此功能。
1 | // 默认行为 |
option:_id
Mongoose 默认给你的 Schema 赋值一个 _id
,这个值的类型是 ObjectId,这与 MongoDB 的默认表现一致。如果需要,可以禁止
此选项只能用于 subdocument。 Mongoose 不能保存没有id的文档,如果硬是要这么做,会报错。
1 | // 默认行为 |
option:minimize
Mongoose 默认不保存空对象。
1 | const schema = new Schema({name:String,inventory:{}}); |
如果把 minimize 设定为 false,Mongoose 将保存空对象
1 | const Sam = new Character({name:'Sam',inventory:{}},{minimize:false}); |
option:read
1 | const schema = new Schema({..}, { read: 'primary' });// also aliased as 'p' |
1 | const options = { replset: { strategy: 'ping' }}; |
option:shardKey
分片相关.
1 | new Schema({ .. }, { shardKey: { tag: 1, name: 1 }}) |
option:strict
Strict 选项默认为 true,这意味着你不能 save
schema 里没有声明的属性。
1 | const thingSchema = new Schema({..}) |
doc.set()
也受该选项影响:
1 | const thingSchema = new Schema({..}) |
这个值可以在 model 级别重写,在第二个参数值传入:
1 | const Thing = mongoose.model('Thing'); |
还可以将strict选项设置为“throw”,这将导致产生错误,而不是删除坏数据。
注意:在 schema 中不存在的实例上设置的任何键/值总是被忽略,不管模式选项是什么。
1 | const thingSchema = new Schema({..}) |
option:strictQuery
1 | const mySchema = new Schema({ field: Number }, { strict: true }); |
严格的选择适用于更新
1 | // Mongoose will strip out `notInSchema` from the update if `strict` is not `false` |
Mongoose 有一个单独的 strictQuery 选项,用于将过滤器参数的strict模式切换到查询。
1 | const mySchema = new Schema({ field: Number }, { |
option:toJSON
与toObject选项完全相同,但仅在调用documents toJSON方法时才适用。
1 | const schema = new Schema({name:String}); |
option:toObject
Documents 的 toObject 方法可以把文档转换成一个 plain javascript object (也就是去掉里面的方法)。 这是一个可以接收多个参数的方法,我们可以在 schemas 定义这些参数。
例如要打印出虚拟值,可以向 toObject
传入 { getters: true }
:
1 | const schema = new Schema({name:String}); |
option:typekey
类型声明
1 | const schema = new Schema({loc:{type:String,coordinates:[Number]}}); |
对于像geoJSON这样的应用程序,“type”属性非常重要。控制使用哪个键mongoose来查找类型声明,要设置“typeKey”模式选项
1 | const schema = new Schema({ |
option:validateBeforeSave
默认情况下,文档在保存到数据库之前会自动验证。这是为了防止保存无效的文档。如果希望手动处理验证,并能够保存没有通过验证的对象,可以将validatebeforeave设置为false。
1 | const schema = new Schema({ name: String }); |
option:versionKey
versionKey
是 Mongoose 在文件创建时自动设定的。 这个值包含文件的内部修订号。 versionKey
是一个字符串,代表版本号的属性名, 默认值为 __v
。如果这个值与你的计划冲突,你可以设定为其他名称:
1 | const schema = new Schema({ name: 'string' }); |
你也可以赋值为 false
禁用 versionKey
。
1 | const Schema({..}, { versionKey: false }); |
option:collation
为 查询(query)和 聚合(aggregation)设置 collation
1 | const schema = new Schema({ |
option:skipVersioning
跳过版本控制允许从版本控制中排除路径(例如。,即使更新了这些路径,也不会增加内部修订)。除非你知道你在做什么,否则不要这样做。对于子文档,使用完全限定路径将其包含在父文档中。
1 | new Schema({..}, { skipVersioning: { dontVersionMe: true } }); |
option:timestamps
如果设置了 timestamps
选项, mongoose 会在你的 schema 自动添加 createdAt
和 updatedAt
字段, 其类型为Date。这两个字段的默认名称是 createAt
和 updateAt
,你可以通过设定 timestamps.createAt
和 timestamps.updateAt
自定义名称。
1 | const thingSchema = new Schema({..}, { timestamps: { createdAt: 'created_at' } }); |
option:useNestedStrict
在 mongoose4 中,update()
和 findOneAndUpdate()
只检查顶层 schema 的严格模式设定。
1 | const childSchema = new Schema({}, { strict: false }); |
如果你把 useNestedStrict
设为 true,mongoose 就不会忽略嵌套的 strict 设定。
1 | const childSchema = new Schema({}, { strict: false }); |
模式类型(SchemaTypes)
SchemaTypes 是处理字段路径各种属性的定义(默认值、验证、getter、setter、查询的字段选择默认值,以及字符串和数字的其他一般特性)
下面是合法的 SchemaTypes:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
- Decimal128
例子:
1 | const schema = new Schema({ |
SchemaType 选项
可以直接声明 schema type 为某一种 type,或者赋值一个含有 type 属性的对象
1 | const schema = new Schema({ |
除了 type 属性,还可以对这个字段路径指定其他属性,如果要在保存之前把字母都改成小写:
1 | const schema2 = new Schema({ |
lowercase
属性只作用于字符串。以下有一些全部type 可用的选项和一些限定部分 type 使用的选项。
全部可用
required
:布尔值或者函数,如果值为真,为此属性添加 required 验证器default
:任何值或者函数设置此路径默认值,如果是函数,函数返回值为默认值select
:布尔值执行query 默认 projectionsvalidate
:属性验证函数get
:函数 使用Object.defineProperty()
定义自定义 getterset
:函数 使用Object.defineProperty()
定义自定义 setteralias
:字符串 仅mongoose >= 4.10.0。 为该字段路径定义虚拟值 gets/sets
1 | const numberSchema = new Schema({ |
索引相关
可以使用 schema type 选项定义 MongoDB indexex
index
:布尔值 是否对这个属性创建索引unique
:布尔值 是否对这个属性创建唯一索引sparse
:布尔值 是否对这个属性创建稀疏索引
1 | const schema = new Schema({ |
String
lowercase
: 布尔值 是否保存前对此值调用.toLowerCase()
uppercase
: 布尔值 是否保存前对此值调用.toUpperCase()
trim
: 布尔值 是否在保存前对此值调用trim()
match
:正则表达式 创建验证器检查这个值是否匹配给定正则表达式enum
:数组 创建验证器检查这个值是否包含于给定数组
Number
min
:数值 创建验证器检查属性是否大于或者等于该值max
:数值 创建验证器检查属性是否小于或者等于该值
Date
min
:Datemax
:Date
使用注意
Dates
内建的 Date 方法不会触发 mongoose 修改跟踪逻辑,如果使用 setMonth()
修改文档里面的 Date
,mongoose 在 doc.save()
的时候是察觉不到这个文档发生了变化的,因此保存不到数据库中。如果一定要用内建的 Date 方法,需要手动调用 doc.markModified('pathToYourDate')
告诉 mongoose 修改了数据。
1 | const Assignment = mongoose.model('Assignment',{dueDate:Date}); |
Mixed
一个什么都可以放的 SchemaType,虽然遍历,但是也会让数据难以维护。Mixed 可以通过 Schema.Types.Mixed 或者传入 一个空对象定义。下面三种方法效果是一致的。
1 | const Any = new Schema({any:{}}); |
因为这是个 schema-less type,所以可以赋值为任意类型,但是 mongoose 无法自动检测并保存修改。要告诉它修改了 Mixed type 的值,调用文档的 .markModified(path)
方法,传入你的 Mixed 字段路径
1 | person.anything = {x:[3,4,{y:'changed'}]} |
ObjectIds
要指定类型为 ObjectId,在声明中使用 Schema.Types.ObjectId
1 | const mongoose = require('mongoose'); |
Arrays
创造 SchemaTypes 或者子文档数组
1 | const ToySchema = new Schema({name:String}); |
注意:指定空数组相当于 Mixed
,下面的操作相当于创建 Mixed
数组:
1 | const Empty1 = new Schema({ary:{]}}); |
数组的默认值是 []
(空数组)
1 | const Toy = mongoose.model('Test',toySchema); |
要手动把默认值设置为 undefined
,从而覆盖 []
1 | const ToySchema = new Schema({ |
创建自定义 Type
Mongoose 可以扩展自定义 SchemaType.
schema.path()
函数
这个函数为给定字段路径返回实例化 schema type
1 | const sampleSchema = new Schema({name:{type:String,require:true}}); |
这个函数可以检查给定字段路径的检查器和类型等信息。
连接(Connections)
可以使用 mongoose.connect()
方法连接 MongoDB
1 | mongoose.connect('mongodb://localhost/myapp'); |
这是连接本地 myapp
数据库默认接口(27017)的最小配置。本地连接失败可以尝试 127.0.0.1.local hostname 被修改有时候会引起问题。
可以在 url 中指定多个参数:
1 | mongoose.connect('mongodb://username:password@host:port/database?options'); |
操作缓存
不必等等连接建立成功就可以使用 Mongoose models
1 | mongoose.connect('mongodb://localhost/myapp'); |
Mongoose 会缓存 model 操作。这个操作很方便,但也会引起一些疑惑,因为如果没有连上的话,Mongoose 不会抛错。
1 | const MyModel = mongoose.model('Test',new Schema({name:String})); |
要禁用缓存,修改 bufferCommands 配置。如果打开了这个选项连接被挂起,尝试关闭 bufferCommands 检查是否正确打开连接,也可以全局禁用 bufferCommands:
1 | mongoose.set('bufferCommands',false); |
选项
connect
方法也接受 options
参数,这些参数会传入底层 MongoDB 驱动
1 | mongoose.connect(uri,options); |
Mongoose 会不做修改直接把选项传到驱动,以下有一点例外
bufferCommands
是 mongoose 特有的选项,user/pass
用于认证的用户名和密码。mongoose 特有的,等价于 MongoDB 驱动的auth.user
和auth.password
选项autoIndex
理想情况下,mongoose 在连接时会自动建立 schema 索引。这有利于开发,但是在大型生产环境中不是很理想,因为会导致性能下降。dbName
指定要连接的数据库名称(覆盖连接字符串)
下面是一些重要的选项
autoReconnect
底层 MongoDB 驱动在连接后将自动重连。除非是管理连接池的高手,不然不建议设置为 falsebufferMaxEntries
MongoDB 驱动同样有自己的离线缓存机制。如果希望连接错误时终止数据库操作,可以设这个值为 0 以及把 bufferCommands 设为 false.promiseLibrary
设置底层 promise 库poolSize
MongoDB 保持的最大 socket 连接数。默认是5.MongoDB 3.4 之前,只允许每个 socket 同时进行一个操作,所以如果有几个缓慢请求卡着后面快的请求,可以尝试增加连接数。
举例子:
1 | const options = { |
回调
connect
函数接受回调函数,或者返回一个 promise
1 | mongoose.connect(uri,options,function(error){ |
连接字符串(Connectng String)选项
可以在连接字符串填写驱动选项,这只适用于 MongoDB 驱动使用的选项,所以类似 bufferCommands 的 Mongoose 专用选项不能在连接字符串使用
1 | mongoose.connect('mongodb://localhost:27017/test?connectTimeoutMS=1000&bufferCommands=false'); |
把选项放在连接字符串的劣势是不便阅读。优势是简短。最佳实践是把区分生产环境和开发环境的选项,socketTimeoutMS
、 connectTimeoutMS
放在 uri , 把通用的常量如 connectTimeoutMS
、 poolSize
放在选项对象里。
keepAlive注意事项
对于长期运行的后台引用,启用毫秒级 keepAlive
是一个精明的操作。不这么做偶尔会经常收到看似没有什么原因的 connection closed
错误
1 | mongoose.connect(uri,{keepAlive:120}); |
副本集(Replica Set)连接
要连接到副本集,可以使用逗号分隔开,传入多个地址:
1 | mongoose.connect('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]' [, options]); |
连接到单节点副本集,需要指定 relicaSet
选项
1 | mongoose.connect('mongodb://host1:port1/?replicaSet=rsName'); |
多 mongos 支持
使用高性能分片集群,需要连接多个 mongos(MongoDB Shard) 实例。 在 mongoose 5.x 中, 你在连接多个 mongos 时不需要传入任何特殊选项。
1 | // Connect to 2 mongos servers |
多个连接
之前我们了解如何使用 Mongoose 默认连接方法连接到 MongoDB。但有时候我们需要权限不同的多个连接, 或是连接到不同数据库。这个情况下我们可以使用 mongoose.createConnection()
, 它接受之前提到的所有参数,给你返回一个新的连接。
1 | const conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options); |
connection对象后续用于创建和检索models。 models 的范围总是局限于单个连接。
调用 mongoose.connect()
时,Mongoose 会自动创建默认连接。 你可以使用 mongoose.connection
访问默认连接。
连接池
无论是使用 mongoose.connect
或是 mongoose.createConnection
创建的连接, 都被纳入默认最大为 5 的连接池,可以通过 poolSize 选项调整:
1 | // With object options |
模型(Models)
Models 是从 Schema 编译来的构造函数,它们的实例就代表着可以从数据保存和读取 documents,从数据创建和读取 document 的所有操作都是通过 model 进行的。
1 | const schema = new Schema({name:'string',size:'string'}); |
第一个参数是跟 model 对应的集合(collection) 名字的单数形式。Mongoose 会自动找到名称是 model 名字复数形式的 collection。对于上例,Tank 这个model 就对应 数据库中 tanks 这个 collection..model()
这个函数是对 schema
做了拷贝(生成了model)。要确保在调用 .model()
之前把所有需要的东西都加进去 schema
里面。
构造 documents
Documents 是 model 的实例,创建它们并保存到数据库非常简单:
1 | const Tank = mongoose.model('Tank',yourSchema); |
要注意,知道 model 使用的数据连接(connection)被打开,tanks 才会被创建/删除,每个 model 都有一个绑定的连接。如果model 是通过调用 mongoose.model()
生成的,它将使用 mongoose 的默认连接。
1 | mongoose.connect('localhost','gettingstarted') |
如果自行创建了连接,就需要使用 connection 的 model
函数代替 mongoose 的 model
函数
1 | const connection = mongoose.createConnection('mongodb://localhost:27017/test'); |
查询
用 mongoose 查询文档相当容易,它支持 mongoDB的高级查询语法(rich),查询文档可以使用 model
的 find,findById,findOne,where 这些静态方法。
1 | Tank.find({size:'small'}).where('createDate').gt(oneYearAgo).exec(callback); |
删除
model
的 remove
方法可以删除所有匹配查询条件的文档
1 | Tank.remove({size:'large'},function(err){ |
更新
model
的 update
方法可以修改数据库中的文档,不过不会把文档返回给应用层。
如果想更新单独一条文档并且返回给应用层,可以使用 findOneAndIUpdate 方法
文档(Documents)
Mongoose documents代表着MongoDB 文档的一对一映射,每个 document 都是他的 Model 实例。
检索
MongoDB 有很多检索数据的方法。
更新
Document 更新的方法同样也有很多,最基本的 findById
1 | Tank.findById(id,function(err,tank){ |
也可以用 .set()
修改 document 。在底层,tank.size = 'large'
,用 tank.set({size:'large'})
1 | Tank.findById(id,function(err,tank){ |
这个方法先检索了数据,接着更新(使用了save
)。如果我们仅仅需要更新而不是获取该数据,model#update
就很适合我们:
1 | Tank.update({_id:id},{$set:{size:'large'}},callback); |
如果我们确实需要返回文档,下面的方法更加适合:
1 | Tank.findByIdAndUpdate(id,{$set:{size:'large'}},{new:true},function(err,tank){ |
findAndUpdate/Remove
系列静态方法查找并返回最多1个文档,模式的方法有不少。
注意:findAndUpdata/Remove
不会修改数据库时执行任何钩子或者验证。可以使用 runValidators
选项获取一个验证的限制子集。但是需要钩子和全文档验证的,还是先query 在save
验证
Document 会在被保存之前验证。
覆盖
可以用 .set()
覆盖整个文档。如果要修改在中间件中被保存的文档,这样就很方便了。
1 | Tank.findById(id,function(err,tank){ |
子文档(Subdocuments)
子文档是指嵌套在另一个文档中的文档。在 Mongoose 中,这意味着可以在 里面嵌套一个 schema 。Mongoose 子文档有两种不同的概念:子文档数组和单个嵌套子文档。
1 | const childSchema = new Schema({name:'string'}); |
子文档和普通 docuemnt 类似。嵌套 schema 可以有自己的中间件,自定义检索逻辑、虚拟值已经其他顶层 schemas 可用的特性,两者主要的不同点是子文档不能单独保存,他们会在他们的顶级文档保存时保存。
1 | const Parent = mongoose.model('Parent',parentSchema); |
子文档跟普通文档一样有 save 和 validate 中间件,调用福文档的 save 会触发所有子文档的 save 中间件, validate 中间件同理。
1 | childSchema.pre('save',function(next){ |
子文档的 pre('save')
和 pre('validate')
中间件执行于顶层 document pre('save')
之前,顶层 document 的 pre('validate')
之后。因为 save()
前的验证就是一个中间件。
1 | const childSchema = new mongoose.Schema({name:'string'}); |
查找子文档
每个子文档都有一个默认 _id
,Mongoose document 数组有一个特别的 id 方法,这个方法只要传入 _id
就能返回文档数组中特定文档。
1 | const doc = parent.children.id(_id); |
添加子文档到数组
Mongoose 数组方法有 push、unshift、addToSet 等等:
1 | const Parent = mongoose.model('Parent'); |
create 方法可以新建文档但是不加入数组
1 | const newdoc = parent.children.create({name:'Haha'}); |
删除子文档
每个子文档都有 remove 方法,另外,对于子文档数组,有一个等效的方法.pull()
。对于单个嵌套子文档,remove 与把这个文档的值设为 null 等效。
1 | // 等效 parent.children.pull(_id) |
代替声明语法的写法
如果用对象的数组创建 schema,mongoose 会自动把对象转成 schema:
1 | const parentSchema = new Schema({ |
查询(queries)
Model 的多个静态辅助方法都可以查询文档。
Model 的方法中包含查询条件参数的(find findById count update)都可以按照下面两种方式执行:
- 传入 callback 参数,操作会被立即执行,查询结果被传给回调函数(callback)
- 不传 callback 参数,Query 的一个实例(一个 query对象)被返回,这个 query 提供了 构建查询器的特殊接口。
Query 实例有一个 .then()
函数,用法类似 promise
如果执行查询时传入 callback,就需要用 JSON 文档的格式指定查询条件,JSON 文档的语法跟 MongoDB shell 一致。
1 | const Parent = mongoose.model('Person',mySchema); |
上面的查询被立即执行,查询结果被传入回调函数。Mongoose 中所有的调用函数都是用 callback(error,result)
这种模式。如果查询发生错误,error
参数即是错误文档,result
参数会是 null。如果查询成功,error
参数是 null,result
即是查询结果。
查询结果的格式取决于做什么操作:findOne() 是单个文档(或者null),find() 是文档列表,count() 是文档数量, update() 是被修改的文档数量。
下面看看不传入 callback 这个参数会是怎么样的:
1 | // 查找每个 last name 是 haha 的 person |
上面的代码中,query 是 Query 类型的变量,Query 能够用链式语法构建查询器,无需要执行 JSON 对象。下面2个实例等效:
1 | Person.find({ |
引用其他文档
MongoDB 中没有表连接,但引用其他结合的文档有时候也会需要。population 就是这样当升的。
Streaming
可以用 流式处理 MongoDB 的查询结果,需要调用 Query.cursor() 函数获得 QueryCursor 的一个实例:
1 | const cursor = Person.find({occupation:/host/}).cursor(); |
验证(validation)
需要先注意下面几点:
- 验证定义于 SchemaType
- 验证是一个中间件,它默认作为
pre(save)
钩子注册在 schema 上 - 可以使用
doc.validate(callback)
或者doc.validateSync()
手动验证 - 验证器不对未定义的值进行验证,唯一例外就是 require 验证器
- 验证是异步递归的。当调用 Model#save,子文档也会执行,出错的话 Model#save 回调会接受错误
- 验证是可以定制的。
1 | const schema = new Schema({ |
内建 Validators
Mongoose 有一些内建验证器
- 所有 SchemaTypes 都有内建的 required 验证器。required 验证器使用
checkRequired()
函数判定这个值是否满足 required 验证器 - Numbers 有 min 和 max 验证器
- String 有 enum、match、maxlength 和 minlength 验证器
上面的链接提供了使用和错误处理相关的详细信息。
1 | const breakfastSchema = new Schema({ |
unique
不是验证器
它是构建 MongoDB unique indexes 的辅助函数
1 | const uniqueUsernameSchema = new Schema({ |
自定义验证器
如果内建检验器不够用了,可以定义满足自己需要的验证器
自定义验证器通过传入一个验证函数来定义
1 | const userSchema = new Schema({ |
异步自定义验证器
自定义验证器可以是异步的,如果检验函数返回 promise (像 async
函数),mongoose 将会等待该 promise 完成。如果更喜欢使用回调函数,设置 isAsync
选项,mongoose 会将回调函数作为验证函数的第二个参数。
1 | const userSchema = new Schema({ |
验证错误
验证失败返回 的err 包含一个 ValidatorError
对象。每一个 ValidatorError 都有 kind、path、value 和 message 属性。ValidatorError 也有可能有 reason 属性如果验证器抛出错误,这个属性会包含该错误原因。
1 | const toySchema = new Schema({ |
嵌套对象中的 Required 验证器
定义嵌套对象的验证器需要特别注意
1 | let personSchema = new Schema({ |
Update 验证器
Mongoose 还支持验证 update() 和 findOneAndUpdate() 操作,Update 验证器默认关闭,如果需要打开,需要配置 runValidators
1 | const toySchema = new Schema({ |
Update 验证器 与 this
update 验证器和 document 验证器有很多不同。上面的颜色验证函数,this 指向验证中的 document,然后update 验证器运行时,被更新文档不一定存在于服务器内存,所以 this 值未定义
1 | const toySchema = new Schema({ |
context
选项
context 选项运行把 update 验证器的 this 设定为 query
1 | toySchema.path('color').validate(function(v){ |
Update 验证器字段路径
另一个关键不同点是 update 验证器只运行于更新的字段。 下例中,因为 ‘name’ 在更新操作未被指定,所以此次更新操作成功。
使用 update 验证器的时候, required
验证器只会在你对某个字段显式使用 $unset
才会触发。
1 | const kittenSchema = new Schema({ |
Update 验证器只运行于指定字段路径
最后要注意的是:update 验证器只运行于下列更新操作:
$set
$unset
$push
(>= 4.8.0)$addToSet
(>= 4.8.0)$pull
(>= 4.12.0)$pullAll
(>= 4.12.0)
例如,以下 update 成功执行,不管 number
的值,因为 update 验证器 无视 $inc
。同样, $push
、$addToSet
、 $pull
和 $pullAll
验证器 不会对数组自身验证,只会对数组中的元素验证。
1 | const testSchema = new Schema({ |
\$push 和 \$addToSet
4.8.0 新特性: update 验证器也运行于 $push
和 $addToSet
1 | const testSchema = new Schema({ |
中间件(middleware)
中间件(pre 和 post)是在异步函数执行时函数传入的控制函数。中间件在 Schema 上指定,在写插件时很有用。Mongoose 4.x 有四种中间件:document、model、aggregate、query 中间件。对于 document 中间件,this 指向当前的 document,Document 中间件支持下面的 document 操作:
- init
- validate
- save
- remove
对于 query中间件,this 指向 query。Query 中间件支持一下 Model 和 Query 操作:
- count
- find
- findOne
- findOneAndRemove
- findOneAndUpdate
- update
Aggregate 中间件作用于 MyModel.aggregat(),它会在对 aggregate 对象调用 exec() 时执行。对于 aggregate 中间件,this 也是指向 aggregation 对象。
- aggregate
对于 model 中间件来说,this 是指向当前的model.Model 中间件支持下面的操作:
- insertMany
所有的中间件支持 pre 和 post 钩子,下面解释这两个钩子的细节。
注意:Query 是没有 remove 钩子的,只有 document有,如果设定了 remove 钩子,将会在调用 myDoc.remove()而不是MyModel.remove 的时候触发。只有 create() 函数会触发 save() 钩子。
Pre
pre 钩子分为串行和并行两种。
串行
串行中间件一个接一个地执行,具体来说,上一个中间件调用 next 函数的时候,下一个执行。
1 | const schema = new Schema({..}); |
在 5.x 版本中,除了手动调用 next ,可以返回一个 promise ,甚至是 async/await。
1 | schema.pre('save',function(){ |
next() 不会阻止剩余代码的运行,可以使用提前 return 模式来阻止 next () 后面的代码。
1 | const schema = new Schema({..}); |
并行
并行中间件提供细粒度流控制
1 | const schema = new Schema({..}); |
上述的例子中,save 方法将在所有中间件都调用了 done 的时候才会执行。
使用场景:
中间件对原子化模型逻辑很有帮助,这里有一些其他建议:
- 复杂的数据校验
- 删除依赖文档(删除用户后删除其他所有文章)
- 某个操作触发的异步任务
错误处理:
如果 pre 钩子出错,momgoose 将不会执行后面的函数。Mongoose 会向回调函数传入 err 参数,或者 reject 返回的 promise 。下面是几个错误的处理的方法:
1 | schema.pre('save',function(next){ |
多次调用 next 是无效的,如果调用 next 带有错误参数 err1 ,然后再抛出一个 err2,mongoose 只会传递 err1
Post中间件
post 中间件在方法执行之后调用,这个时候每个 pre 中间件都已经完成了。
1 | schema.post('init', function(doc) { |
异步 Post 钩子
如果给回调函数传入两个参数,mongoose 会认为第二个参数 next 函数,可以通过 next 触发下一个中间件
1 | schema.post('save',function(doc,next){ |
Save/Validate钩子
save() 函数触发 validate 钩子,mongoose validate() 其实就是 pre(‘save’)钩子,这意味着所有 pre(‘validate’) 和 post(‘validate’)都会在 pre(‘save’)钩子之前调用。
1 | schema.pre('validate',function(){ |
finAndUpdate() 与 Query 中间件使用注意
pre 和 post save() 钩子都不执行于 update ,findOneAndUpdate 等情况。Mongoose4.0 为这些函数制定了新钩子。
1 | schema.pre('find',function(){ |
Query 中间件不同于 document 中间件,document 中间件中,this 指向被更新 document,query 中间件中,this 指向query 对象而不是被更新 document
例如,如果要每次 update 之前更新 updateAt 时间戳,可以使用 pre 钩子
1 | schema.pre('update',function(){ |
错误处理中间件
错误处理中间件比普通中间件多一个 error
参数,并且 err
作为第一个参数传入。 而后错误处理中间件可以让你自由地做错误的后续处理。
1 | const schema = new Schema({ |
对于query 中间件也可以使用错误处理,可以定义一个 post update 钩子,它可以捕获 MongoDB 重复 key 错误。
1 | schema.post('update',function(error,res,next){ |
填充(Populate)
像sql 里面的 join 的聚合操作,populate 可以让你在别的 collection 中引用 document
Population 可以自动替换 document 中的指定字段,替换内容从其他 collection 获取。我们可以填充单个或者每个 document、单个或者多个纯对象,甚至是 query 返回的一切对象。
1 | const mongoose = require('mongoose'); |
上面创建了两个 Model,Person model 的 stories 字段设为 ObjectId 数组。ref 选项告诉 Mongoose 在填充的时候使用哪个 model,上例中为 Story 的 model.所有存储在此的 _id 必须是 Story model 中的 document 的 _id。
注意:ObjectId、Number、String 以及 Buffer 都可以作为 refs 使用。但是最好还是用 ObjectId
保存 refs
保存 refs 与保存普通属性一样,把 _id 的值赋给它就好了:
1 | const author = new Person({ |
Population
我们做的东西还是跟平时差不多,只是创建了 Person
和 Story
。现在试试对query 填充 story 的 author。
1 | Story.findOne({ |
被填充的字段已经不再是原来的 _id,而是被指定的 document 替代,这个 document 由另一条 query 从数据库返回。refs 数组的原理也是相似的。对 query 对象调用 populate 方法,就能返回装载对应 _id 的 document 数组。
设置被填充字段
1 | Story.findOne({ title: 'Casino Royale' }, function(error, story) { |
字段选择
如果我们只需要填充 document 其中一部分字段,第二个参数传入 field name syntax 就可以实现。
1 | Story. |
填充多个字段
1 | Story. |
如果对同一路径 populate 两次,只有最后一次生效。
1 | // 第二个会覆盖第一个的 |
Query 条件与其他选项
如果要根据年龄来填充,只填充 name,并且值返回最多5个数据
1 | Story. |
Refs 到 children
我们发现,用 author 对象没有办法获取 story 列表,因为 author.stories 没有被 pushed 任何 story 对象
我们希望 author 知道哪些 story 属于他们,通常,schema 应该通过在 多 的一方使用指向它们的父节点(parent pointer)解决一对多关系问题。另一方面,如果有充分理由得到指向子节点的数组,可以像下面代码一样把 document push() 到数组中。
1 | author.stories.push(story1); |
然后我们就能 find 和 populate 了
1 | Person.findOne({name:'Ian Fleming'}). |
如果父子节点互相指向,数据可能会在某一时刻失去同步。为此我们可以不使用填充,直接 find()
我们需要的 story
1 | Story.find({author:author._id}). |
query 填充后返回的 document 功能齐全,除非设置 lean 选项,否则它就是可 remove,可 save 的。调用 remove 不单是从数组中删除也是从数据库中删除它们。
填充现有 document
document#populate()
填充多个现有 document
如果需要填充一个或者多个 document 对象,我们可以使用 Model.populate 方法。
多级填充
假设 user schema 记录了 user 的 friends
1 | const userSchema = new Schema({ |
当然可以填充得到用户的 friends 列表,但是如果要在获得他们朋友的朋友呢?指定 populate
选项就可以了
1 | User.findOne({name:'Val'}). |
跨数据库填充
假设现在有 event schema 和 conversation schema,每个 event 对应一个 conversation 线程
1 | const eventSchema = new Schema({ |
并且,event 和 conversation 保存在不同的 MongoDB 实例中
1 | const db1 = mongoose.createConnection('localhost:27000/db1'); |
这个情况就不能直接使用 populate()
了,因为 populate()
不知道应该使用什么填充。 可以显式指定一个 model
1 | Event.find().population({path:'conversation',model:Conversation}). |
动态引用
Mongoose 也可以从多个 collection 填充。假设 user schema 有一系列 connection,一个 user 可以连接到其他 user 或者组织。
1 | const userSchema = new Schema({ |
上面的 refPath
属性意味着 mongoose 会查找 connections.kind
路径, 以此确定 populate()
使用的 model。换句话说,refPath
属性可以让你动态寻找 ref
。
鉴别器(Discriminators)
Discriminator 是一种 schema 继承机制。它允许在相同的底层 MongoDB collection 上使用部分重叠的 schema 建立多个 model。
假设要在单个 collection 中记录多种 event ,每个 event 都有时间戳字段,但是 click 事件还有 URL 字段,可以用 model.discriminator 实现上面的要求。这个函数需要两个参数,model 名称和 discriminator schema,返回 model 结合原 model 的 schema 和 discriminator schema.
1 | const options = { discriminatorKey:'kind' } |
Discriminator 储存在 Event model 的 collection
现在假设你要创建另一个 discriminator,记录用户注册 event。 SignedUpEvent
实例将跟 通用 events 和 ClickedLinkEvent
实例 一样储存在同一个 collection。
1 | const event1 = new Event({time: Date.now()}); |
Discriminator keys
Mongoose 通过 ‘discriminator key’ 识别两个不同的 discriminator, 这个值默认是 __t
。Mongoose 自动在你的 schema 添加 __t
字段, 记录你的 document 是哪个 discriminator 的实例。
1 | const event1 = new Event({time: Date.now()}); |
Discriminator 在查询中添加 discriminator key
Discriminator model 的特别之处在于:他们会把 discriminator key 附到 query 上。换句话说,find()
, count()
, aggregate()
等方法 都能适配 discriminators。
1 | const event1 = new Event({time: Date.now()}); |
Discriminator 复制 pre / post 钩子
Discriminator 会继承他的基础 schema 的 pre 和 post 中间件。 不过,你也可以为 discriminator 添加中间件,这不回影响到基础 schema。
1 | const options = {discriminatorKey: 'kind'}; |
处理自定义 _id 字段
Discriminator 的字段是基础 schema 加 discriminator schema , 并且以 discriminator schema 的字段优先。 但有一个例外,_id
字段。
You can work around this by setting the _id
option to false in the discriminator schema as shown below.
1 | const options = {discriminatorKey: 'kind'}; |
discriminator 与 Model.create()
当你使用 Model.create()
,Mongoose 会自动帮你适配 discriminator key ~
1 | const Schema = mongoose.Schema; |
数组中的嵌套 discriminator
你也可以为嵌套文档数组定义 discriminator。 嵌套 discriminator 的特点是:不同 discriminator 类型储存在相同的文档而不是同一个 mongoDB collection。 换句话说,嵌套 discriminator 让你 在同一个数组储存符合不同 schema 的子文档。
最佳实践:确保你声明了钩子再使用他们。 你不应当在调用 discriminator()
之后调用 pre()
或 post()
1 | const eventSchema = new Schema({ message: String }, |
检索数组中的嵌套 discriminator
检索嵌套 discriminator
1 | const singleEventSchema = new Schema({ message: String }, |
###