引言
继上篇文章「
Koa2+MongoDB+JWT实战--Restful API最佳实践
」后,收到许多小伙伴的反馈,表示自己对于
mongoose
不怎么了解,上手感觉有些难度,看官方文档又基本都是英文(宝宝心里苦,但宝宝不说

)。
为了让各位小伙伴快速上手,加深对于 mongoose 的了解,我特地结合之前的项目整理了一下关于 mongoose 的一些基础知识,这些对于实战都是很有用的。相信看了这篇文章,一定会对你快速上手,了解使用 mongoose 有不小的帮助。
mongoose 涉及到的概念和模块还是很多的,大体有下面这些:
本篇文章并不会逐个去展开详细讲解,主要是讲述在实战中比较重要的几个模块:
模式(schemas)
、
模式类型(SchemaTypes)
、
连接(Connections)
、
模型(Models)
和
联表(Populate)
。
模式(schemas)
定义你的 schema
Mongoose
的一切都始于一个
Schema
。每个 schema 映射到 MongoDB 的集合(
collection
)和定义该集合(collection)中的文档的形式。
const mongoose = require("mongoose");
const { Schema, model } = mongoose;
const userSchema = new Schema(
{
__v: { type: Number, select: false },
name: { type: String, required: true },
password: { type: String, required: true, select: false },
avatar_url: { type: String },
gender: {
type: String,
enum: ["male", "female"],
default: "male",
required: true
},
headline: { type: String },
},
{ timestamps: true }
);
module.exports = model("User", userSchema);
复制
这里的是
__v
。该 versionKey 是每个文档首次创建时,由 mongoose 创建的一个属性。包含了文档的内部修订版。此文档属性是可配置的。默认值为
versionKey
。如果不需要该版本号,在 schema 中添加
__v
即可。
{ versionKey: false}
创建模型
使用我们的 schema 定义,我们需要将我们的
userSchema
转成我们可以用的模型。也就是
mongoose.model(modelName, schema)
。也就是上面代码中的:
module.exports = model("User", userSchema);
复制
选项(options)
Schemas 有几个可配置的选项,可以直接传递给构造函数或设置:
new Schema({..}, options);
// or
var schema = new Schema({..});
schema.set(option, value);
复制
可用选项:
-
autoIndex
-
bufferCommands
-
capped
-
collection
-
id
-
_id
-
minimize
-
read
-
shardKey
-
strict
-
toJSON
-
toObject
-
typeKey
-
validateBeforeSave
-
versionKey
-
skipVersioning
-
timestamps
这里我只是列举了常用的配置项,完整的配置项可查看官方文档
https://mongoosejs.com/docs/guide.html#options
。
这里我主要说一下
versionKey
和
timestamps
:
-
(上文有提到) 是 Mongoose 在文件创建时自动设定的。这个值包含文件的内部修订号。versionKey 是一个字符串,代表版本号的属性名, 默认值为versionKey
__v
- 如果设置了
选项, mongoose 会在你的 schema 自动添加timestamps
和createdAt
字段, 其类型为updatedAt
。Date
到这里,已经基本介绍完了
Schema
,接下来看一下
SchemaTypes
模式类型(SchemaTypes)
SchemaTypes
为查询和其他处理路径默认值,验证,getter,setter,字段选择默认值,以及字符串和数字的特殊字符。在 mongoose 中有效的 SchemaTypes 有:
-
String
-
Number
-
Date
-
Buffer
-
Boolean
-
Mixed
-
ObjectId
-
Array
-
Decimal128
-
Map
看一个简单的示例:
const answerSchema = new Schema(
{
__v: { type: Number, select: false },
content: { type: String, required: true },
answerer: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
select: false
},
questionId: { type: String, required: true },
voteCount: { type: Number, required: true, default: 0 }
},
{ timestamps: true }
);
复制
所有的 Schema 类型
-
: 布尔值或函数,如果为 true,则为此属性添加必须的验证。required
-
: 任意类型或函数,为路径设置一个默认的值。如果值是一个函数,则函数的返回值用作默认值。default
-
: 布尔值 指定 query 的默认select
projections
-
: 函数,对属性添加验证函数。validate
-
: 函数,使用get
定义自定义 getterObject.defineProperty()
-
: 函数,使用set
定义自定义 setterObject.defineProperty()
-
: 字符串,只对alias
有效。定义一个具有给定名称的虚拟属性,该名称可以获取/设置这个路径mongoose>=4.10.0
索引
你可以用 schema 类型选项声明 MongoDB 的索引。
-
: 布尔值,是否在属性中定义一个索引。index
-
: 布尔值,是否在属性中定义一个唯一索引。unique
-
: 布尔值,是否在属性中定义一个稀疏索引。sparse
var schema2 = new Schema({
test: {
type: String,
index: true,
unique: true // 如果指定`unique`为true,则为唯一索引
}
});
复制
字符串
-
: 布尔值,是否在保存前对此值调用lowercase
toLowerCase()
-
: 布尔值,是否在保存前对此值调用uppercase
toUpperCase()
-
: 布尔值,是否在保存前对此值调用trim
trim()
-
: 正则,创建一个验证器,验证值是否匹配给定的正则表达式match
-
: 数组,创建一个验证器,验证值是否是给定数组中的元素enum
数字
-
: 数字,创建一个验证器,验证值是否大于等于给定的最小值min
-
: 数字,创建一个验证器,验证值是否小于等于给定的最大的值max
日期
-
: Datemin
-
: Datemax
现在已经介绍完
Schematype
,接下来让我们看一下
Connections
。
连接(Connections)
我们可以通过利用
mongoose.connect()
方法连接 MongoDB 。
mongoose.connect('mongodb://localhost:27017/myapp');
复制
这是连接运行在本地
myapp
数据库最小的值(
27017
)。如果连接失败,尝试用
127.0.0.1
代替
localhost
。
当然,你可在 uri 中指定更多的参数:
mongoose.connect('mongodb://username:password@host:port/database?options...');
复制
操作缓存
意思就是我们不必等待连接建立成功就可以使用 models,mongoose 会先缓存 model 操作
let TestModel = mongoose.model('Test', new Schema({ name: String }));
// 连接成功前操作会被挂起
TestModel.findOne(function(error, result) { /* ... */ });
setTimeout(function() {
mongoose.connect('mongodb://localhost/myapp');
}, 60000);
复制
如果要禁用缓存,可修改
bufferCommands
配置,也可以全局禁用 bufferCommands
mongoose.set('bufferCommands', false);
复制
选项
connect 方法也接收一个 options 对象:
mongoose.connect(uri, options);
复制
这里我列举几个在日常使用中比较重要的选项,完整的连接选项看这里
-
:这是 mongoose 中一个特殊的选项(不传递给 MongoDB 驱动),它可以禁用 mongoose 的bufferCommands
。缓冲机制
-
:身份验证的用户名和密码。这是 mongoose 中特殊的选项,它们可以等同于 MongoDB 驱动中的user/pass
和auth.user
选项。auth.password
-
:指定连接哪个数据库,并覆盖连接字符串中任意的数据库。dbName
-
:底层 MongoDB 已经废弃当前连接字符串解析器。因为这是一个重大的改变,添加了 useNewUrlParser 标记如果在用户遇到 bug 时,允许用户在新的解析器中返回旧的解析器。useNewUrlParser
-
:MongoDB 驱动将为这个连接保持的最大 socket 数量。默认情况下,poolSize 是 5。poolSize
-
:默认情况下为useUnifiedTopology
。设置为 true 表示选择使用 MongoDB 驱动程序的新连接管理引擎。您应该将此选项设置为 true,除非极少数情况会阻止您保持稳定的连接。false
示例:
const options = {
useNewUrlParser: true,
useUnifiedTopology: true,
autoIndex: false, // 不创建索引
reconnectTries: Number.MAX_VALUE, // 总是尝试重新连接
reconnectInterval: 500, // 每500ms重新连接一次
poolSize: 10, // 维护最多10个socket连接
// 如果没有连接立即返回错误,而不是等待重新连接
bufferMaxEntries: 0,
connectTimeoutMS: 10000, // 10s后放弃重新连接
socketTimeoutMS: 45000, // 在45s不活跃后关闭sockets
family: 4 // 用IPv4, 跳过IPv6
};
mongoose.connect(uri, options);
复制
回调
connect()
函数也接收一个回调参数,其返回一个 promise。
mongoose.connect(uri, options, function(error) {
// 检查错误,初始化连接。回调没有第二个参数。
});
// 或者用promise
mongoose.connect(uri, options).then(
() => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ },
err => { /** handle initial connection error */ }
);
复制
说完
Connections
,下面让我们来看一个重点
Models
模型(Models)
Models
是从
Schema
编译来的构造函数。它们的实例就代表着可以从数据库保存和读取的
documents
。从数据库创建和读取 document 的所有操作都是通过
model
进行的。
const mongoose = require("mongoose");
const { Schema, model } = mongoose;
const answerSchema = new Schema(
{
__v: { type: Number, select: false },
content: { type: String, required: true },
},
{ timestamps: true }
);
module.exports = model("Answer", answerSchema);
复制
定义好 model 之后,就可以进行一些增删改查操作了
创建
如果是
Entity
,使用
save
方法;如果是
Model
,使用
create
方法或
insertMany
方法。
// save([options], [options.safe], [options.validateBeforeSave], [fn])
let Person = mongoose.model("User", userSchema);
let person1 = new Person({ name: '森林' });
person1.save()
// 使用save()方法,需要先实例化为文档,再使用save()方法保存文档。而create()方法,则直接在模型Model上操作,并且可以同时新增多个文档
// Model.create(doc(s), [callback])
Person.create({ name: '森林' }, callback)
// Model.insertMany(doc(s), [options], [callback])
Person.insertMany([{ name: '森林' }, { name: '之晨' }], function(err, docs) {
})
复制
说到这里,我们先要补充说明一下 mongoose 里面的三个概念:
schema
、
model
和
entity
:
-
: 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力schema
-
: 由 schema 发布生成的模型,具有抽象属性和行为的数据库操作对model
-
: 由 Model 创建的实体,他的操作也会影响数据库entity
Schema、Model、Entity 的关系请牢记: Schema生成Model,Model创造Entity
,Model 和 Entity 都可对数据库操作造成影响,但 Model 比 Entity 更具操作性。
查询
对于 Mongoosecha 的查找文档很容易,它支持丰富的查询 MongoDB 语法。包括
find
、
findById
、
findOne
等。
find()
find()
第一个参数表示查询条件,第二个参数用于控制返回的字段,第三个参数用于配置查询参数,第四个参数是回调函数,回调函数的形式为
function(err,docs){}
Model.find(conditions, [projection], [options], [callback])
复制
下面让我们依次看下 find()的各个参数在实际场景中的应用:
- conditionsModel.find({})Model.find({name:'森林'})对比相关操作符符号描述eq与指定的值相等ne与指定的值不相等gt大于指定的值gte大于等于指定的值lt小于指定的值lte小于等于指定的值in与查询数组中指定的值中的任何一个匹配nin与查询数组中指定的值中的任何一个都不匹配Model.find({ age: { in: [18, 24]} })返回 age 字段等于 18 或者 24 的所有 document。逻辑相关操作符符号描述and满足数组中指定的所有条件nor不满足数组中指定的所有条件or满足数组中指定的条件的其中一个not反转查询,返回不满足指定条件的文档// 返回 age 字段大于 24 或者 age 字段不存在的文档Model.find( { age: { not: { lte: 24 }}})字段相关操作符符号描述exists匹配存在指定字段的文档type返回字段属于指定类型的文档数组字段的查找符号描述all匹配包含查询数组中指定的所有条件的数组字段elemMatch匹配数组字段中的某个值满足 elemMatch 中指定的所有条件size匹配数组字段的 length 与指定的大小一样的 document// 使用 all 查找同时存在 18 和 20 的 documentModel.find({ age: {
- 使用操作符
- 精确查找
- 查找全部
-
指定要包含或排除哪些projection
字段(也称为查询“document
”),必须同时指定包含或同时指定排除,不能混合指定,投影
_id
除外。
在 mongoose 中有两种指定方式,
和字符串指定
对象形式指定
。
字符串指定时在排除的字段前加 - 号,只写字段名的是包含。
Model.find({},'age');
对象形式指定时,Model.find({},'-name');
是包含, 是排除。1
Model.find({}, { age: 1 });
Model.find({}, { name: 0 });
-
options
// 三种方式实现
Model.find(filter,null,options)
Model.find(filter).setOptions(options)
Model.find(filter).<option>(xxx)
options 选项见官方文档 Query.prototype.setOptions()。
这里我们只列举常用的:
// sort 两种方式指定排序
Model.find().sort('age -name'); // 字符串有 - 代表 descending 降序
Model.find().sort({age:'asc', name:-1});
和sort
同时使用时,调用的顺序并不重要,返回的数据都是先排序后限制数量。limit
// 效果一样
Model.find().limit(2).sort('age');
Model.find().sort('age').limit(2);
-
: 按照排序规则根据所给的字段进行排序,值可以是 asc, desc, ascending, descending, 1, 和 -1。sort
-
: 指定返回结果的最大数量limit
-
: 指定要跳过的文档数量skip
-
: 返回普通的 js 对象,而不是lean
。建议不需要 mongoose 特殊处理就返给前端的数据都最好使用该方法转成普通 js 对象。Mongoose Documents
-
-
Mongoose 中所有传入callback
的查询,其格式都是callback
这种形式。如果出错,则 error 是出错信息,result 是 null;如果查询成功,则 error 是 null, result 是查询结果,查询结果的结构形式是根据查询方法的不同而有不同形式的。callback(error, result)
方法的查询结果是数组,即使没查询到内容,也会返回 [] 空数组。find()
findById
findById
Model.findById(id,[projection],[options],[callback])
复制
Model.findById(id)
相当于
Model.findOne({ _id: id })
。
看一下官方对于
findOne
与
findById
的对比:
不同之处在于处理 id 为时的情况。
undefined
相当于
findOne({ _id: undefined })
,返回任意一条数据。而
findOne({})
相当于
findById(undefined)
,返回
findOne({ _id: null })
。
null
查询结果:
- 返回数据的格式是
对象形式。{}
- id 为
或undefined
,result 返回null
。null
- 没符合查询条件的数据,result 返回
。null
findOne
findOne
该方法返回查找到的所有实例的第一个
Model.findOne(conditions, [projection], [options], [callback])
复制
如果查询条件是
_id
,建议使用
findById()
。
查询结果:
- 返回数据的格式是
对象形式。{}
- 有多个数据满足查询条件的,只返回第一条。
- 查询条件 conditions 为 {}、 null 或 undefined,将任意返回一条数据。
- 没有符合查询条件的数据,result 返回 null。
更新
每个模型都有自己的更新方法,用于修改数据库中的文档,不将它们返回到您的应用程序。常用的有
findOneAndUpdate()
、
findByIdAndUpdate()
、
update()
、
updateMany()
等。
findOneAndUpdate()
findOneAndUpdate()
Model.findOneAndUpdate(filter, update, [options], [callback])
复制
-
查询语句,和filter
find()
一样。
filter 为
,则只更新第一条数据。{}
- update{operator: { field: value, ... }, ... }必须使用 update 操作符。如果没有操作符或操作符不是 update 操作符,统一被视为 set 操作(mongoose 特有)字段相关操作符符号描述set设置字段值currentDate设置字段值为当前时间,可以是 Date 或时间戳格式。min只有当指定值小于当前字段值时更新max只有当指定值大于当前字段值时更新inc将字段值增加指定数量,指定数量可以是负数,代表减少。mul将字段值乘以指定数量unset删除指定字段,数组中的值删后改为 null。数组字段相关操作符符号描述充当占位符,用来表示匹配查询条件的数组字段中的第一个元素 {operator:{ "arrayField.addToSet向数组字段中添加之前不存在的元素 { addToSet: {arrayField: value, ... }},value 是数组时可与 each 组合使用。push向数组字段的末尾添加元素 { push: { arrayField: value, ... } },value 是数组时可与 each 等修饰符组合使用pop移除数组字段中的第一个或最后一个元素 { pop: {arrayField: -1(first) / 1(last), ... } }pull移除数组字段中与查询条件匹配的所有元素 { pull: {arrayField: value / condition, ... } }pullAll从数组中删除所有匹配的值 { pullAll: { arrayField: [value1, value2 ... ], ... } }修饰符符号描述each修饰 push 和 addToSet 操作符,以便为数组字段添加多个元素。position修饰 push 操作符以指定要添加的元素在数组中的位置。slice修饰 push 操作符以限制更新后的数组的大小。sort修饰
- 在指定的位置添加元素以更新数组字段
- 按照指定的规则排序
- 限制数组大小
- 存储数组
-
options
- lean: true 返回普通的 js 对象,而不是
。Mongoose Documents
- new: 布尔值,
返回更新后的数据,true
(默认)返回更新前的数据。false
- fields/select:指定返回的字段。
- sort:如果查询条件找到多个文档,则设置排序顺序以选择要更新哪个文档。
- maxTimeMS:为查询设置时间限制。
- upsert:布尔值,如果对象不存在,则创建它。默认值为
。false
- omitUndefined:布尔值,如果为
,则在更新之前删除值为true
的属性。undefined
- rawResult:如果为
,则返回来自 MongoDB 的原生结果。true
- lean: true 返回普通的 js 对象,而不是
-
callback
- 没找到数据返回
null
- 更新成功返回更新前的该条数据(
形式){}
-
的options
,更新成功返回更新后的该条数据({new:true}
形式){}
- 没有查询条件,即
为空,则更新第一条数据filter
- 没找到数据返回
findByIdAndUpdate()
findByIdAndUpdate()
Model.findByIdAndUpdate(id, update, options, callback)
复制
Model.findByIdAndUpdate(id, update)
相当于
Model.findOneAndUpdate({ _id: id }, update)
。
result 查询结果:
- 返回数据的格式是
对象形式。{}
- id 为
或undefined
,result 返回null
。null
- 没符合查询条件的数据,result 返回
。null
update()
update()
Model.update(filter, update, options, callback)
复制
-
options
- multi: 默认
,只更新第一条数据;为false
时,符合查询条件的多条文档都会更新。true
- overwrite:默认为 false,即 update 参数如果没有操作符或操作符不是 update 操作符,将会默认添加 set;如果为 true,则不添加 set,视为覆盖原有文档。
- multi: 默认
updateMany()
updateMany()
Model.updateMany(filter, update, options, callback)
复制
更新符合查询条件的所有文档,相当于
Model.update(filter, update, { multi: true }, callback)
删除
删除常用的有
findOneAndDelete()
、
findByIdAndDelete()
、
deleteMany()
、
findByIdAndRemove()
等。
findOneAndDelete()
findOneAndDelete()
Model.findOneAndDelete(filter, options, callback)
复制
-
查询语句和filter
一样find()
-
options
- sort:如果查询条件找到多个文档,则设置排序顺序以选择要删除哪个文档。
- select/projection:指定返回的字段。
- rawResult:如果为
,则返回来自true
的原生结果。MongoDB
-
callback
- 没有符合
的数据时,返回filter
。null
-
为空或filter
时,删除第一条数据。{}
- 删除成功返回
形式的原数据。{}
- 没有符合
findByIdAndDelete()
findByIdAndDelete()
Model.findByIdAndDelete(id, options, callback)
复制
Model.findByIdAndDelete(id)
相当于
Model.findOneAndDelete({ _id: id })
。
-
callback
- 没有符合
的数据时,返回id
。null
-
为空或id
时,返回undefined
。null
- 删除成功返回
形式的原数据。{}
- 没有符合
deleteMany()
deleteMany()
Model.deleteMany(filter, options, callback)
复制
-
删除所有符合filter
条件的文档。filter
deleteOne()
deleteOne()
Model.deleteOne(filter, options, callback)
复制
-
删除符合filter
条件的第一条文档。filter
findOneAndRemove()
findOneAndRemove()
Model.findOneAndRemove(filter, options, callback)
复制
用法与
findOneAndDelete()
一样,一个小小的区别是
findOneAndRemove()
会调用 MongoDB 原生的
findAndModify()
命令,而不是
findOneAndDelete()
命令。
建议使用
findOneAndDelete()
方法。
findByIdAndRemove()
findByIdAndRemove()
Model.findByIdAndRemove(id, options, callback)
复制
Model.findByIdAndRemove(id)
相当于
Model.findOneAndRemove({ _id: id })
。
remove()
remove()
Model.remove(filter, options, callback)
复制
从集合中删除所有匹配
filter
条件的文档。要删除第一个匹配条件的文档,可将
single
选项设置为
true
。
看完
Models
,最后让我们来看下在实战中比较有用的
Populate
联表(Populate)
Mongoose 的
populate()
可以
连表查询
,即在另外的集合中引用其文档。
Populate()
可以自动替换
document
中的指定字段,替换内容从其他
collection
中获取。
refs
创建
Model
的时候,可给该
Model
中关联存储其它集合
_id
的字段设置
ref
选项。
ref
选项告诉
Mongoose
在使用
populate()
填充的时候使用哪个
Model
。
const mongoose = require("mongoose");
const { Schema, model } = mongoose;
const answerSchema = new Schema(
{
__v: { type: Number, select: false },
content: { type: String, required: true },
answerer: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
select: false
},
questionId: { type: String, required: true },
voteCount: { type: Number, required: true, default: 0 }
},
{ timestamps: true }
);
module.exports = model("Answer", answerSchema);
复制
上例中
Answer
model 的
answerer
字段设为 ObjectId 数组。ref 选项告诉 Mongoose 在填充的时候使用
User
model。所有储存在
answerer
中的
_id
都必须是
User
model 中
document
的
_id
。
ObjectId
、
Number
、
String
以及
Buffer
都可以作为
refs
使用。但是最好还是使用
ObjectId
。
在创建文档时,保存
refs
字段与保存普通属性一样,把
_id
的值赋给它就好了。
const Answer = require("../models/answers");
async create(ctx) {
ctx.verifyParams({
content: { type: "string", required: true }
});
const answerer = ctx.state.user._id;
const { questionId } = ctx.params;
const answer = await new Answer({
...ctx.request.body,
answerer,
questionId
}).save();
ctx.body = answer;
}
复制
populate(path,select)
填充document
填充document
const Answer = require("../models/answers");
const answer = await Answer.findById(ctx.params.id)
.select(selectFields)
.populate("answerer");
复制
被填充的
answerer
字段已经不是原来的
_id
,而是被指定的
document
代替。这个
document
由另一条
query
从数据库返回。
返回字段选择
返回字段选择
如果只需要填充
document
中一部分字段,可给
populate()
传入第二个参数,参数形式即
返回字段字符串
,同 Query.prototype.select()。
const answer = await Answer.findById(ctx.params.id)
.select(selectFields)
.populate("answerer", "name -_id");
复制
populate 多个字段
populate 多个字段
const populateStr =
fields &&
fields
.split(";")
.filter(f => f)
.map(f => {
if (f === "employments") {
return "employments.company employments.job";
}
if (f === "educations") {
return "educations.school educations.major";
}
return f;
})
.join(" ");
const user = await User.findById(ctx.params.id)
.select(selectFields)
.populate(populateStr);
复制
最后
到这里本篇文章也就结束了,这里主要是结合我平时的项目(
https://github.com/Jack-cool/rest_node_api
)中对于
mongoose
的使用做的简单的总结。希望能给你带来帮助