安装依赖
npm install express --save
npm install mongoose --save
npm install cors --save
npm install morgan --save
连接数据库
app.js
if (process.env.NODE_ENV !== "production") {
require("dotenv").config();
}
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const mongoose = require("mongoose");
const app = express();
const resourceMiddleware = require("./middleware/resource");
// 日志
app.use(morgan("combined"));
// 解析 body 数据,只有在 post 和 put 方法中才会用到
app.use(express.json()); // for parsing application/json
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// 允许跨域
app.use(cors());
// rest
const restRouter = require("./routes/rest/index");
app.use("/rest/:resource", resourceMiddleware(), restRouter);
app.use("/", require("./routes/test"));
// 数据库
mongoose.set("useCreateIndex", true);
mongoose.connect(`mongodb://${process.env.DB_HOST}/${process.env.DB_NAME}`, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: true,
});
const db = mongoose.connection;
db.on("error", (err) => console.log(err));
db.once("open", () => {
console.log("database connected");
require("require-all")(__dirname + "/models");
app.listen(5000);
});
routes/authors.js
const express = require("express");
const router = express.Router();
const Author = require("../models/Author");
function randomStr() {
return Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")
.substr(0, 5);
}
module.exports = router;
如果设置了密码,则连接字符串应该改为mongodb://user:pass@localhost:port/database
创建Schema
models/Author.js
const mongoose = require("mongoose");
const authorSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
});
module.exports = mongoose.model("Author", authorSchema);
// 第一个参数指定model名字,在数据库中对应表的名字为 authors (小写 + 复数),也可以通过设置第三个参数指定表的名字
字段默认值
const authorSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
age: {
type: Number,
default: 0,
},
});
预定义模式修饰符
可以对增加的数据进行格式化
trim
删除左右两边的空格
lowercase
小写
uppercase
大写
const authorSchema = new mongoose.Schema({
name: {
type: String,
// 是否必须
required: true,
// 删除左右两边的空格
trim: true,
},
age: {
type: Number,
default: 0,
},
});
自定义修饰符 Getters、Setters
Setter
const authorSchema = new mongoose.Schema({
name: {
type: String,
// 是否必须
required: true,
// 删除左右两边的空格
trim: true,
},
age: {
type: Number,
default: 0,
},
books: {
type: Array,
set(val) {
if (typeof val === "string") return val.split(",");
return val;
},
get(val) {
return val.join(",");
},
},
});
先判断参数是否为字符串类型,如果是,则将其转化为数组,否则直接返回(使用create
方法保存数据时,setter
方法会调用两次)。
Getter
值得一提的是,getter
不会修改数据库中返回的数据,而是在获得doc对象后,再访问其中的属性时候才会生效
index
设置索引
const authorSchema = new mongoose.Schema({
name: {
type: String,
// 是否必须
required: true,
// 删除左右两边的空格
trim: true,
// 设置索引
index: true,
}
});
unique
是否唯一,如果设置了unique
为true,就没有必要设置index
const authorSchema = new mongoose.Schema({
name: {
type: String,
// 是否必须
required: true,
// 删除左右两边的空格
trim: true,
// 唯一
unique: true,
}
});
数据校验
- required 表示这个数据必须传入
- max 用于Number类型数据,设置最大值
- min 用于Number类型数据,设置最小值
- enum 枚举类型,要求必须满足枚举值
enum:['1', '2', '3']
- match 正则校验
- maxlength 最大长度
- minlength 最小长度
- 自定义验证
示例
const AuthorSchema = new mongoose.Schema({
name: {
type: String,
required: true,
maxlength: 8,
minlength: 2,
},
sn: {
type: String,
match: /^sn(.*)/,
},
age: {
type: Number,
min: 0,
max: 150,
},
status: {
type: String,
default: "0",
enum: ["0", "1", "2"], // 取值必须在枚举值中,只校验字符类型
}
});
自定义验证
const AuthorSchema = new mongoose.Schema({
sn: {
type: String,
validate: (val) => {
return val.length > 10;
},
},
});
CURD方法
内置方法
Model.deleteMany()
Model.deleteOne()
Model.find()
Model.findById()
Model.findByIdAndDelete()
Model.findByIdAndRemove()
Model.findByIdAndUpdate()
Model.findOne()
Model.findOneAndDelete()
Model.findOneAndRemove()
Model.findOneAndReplace()
Model.findOneAndUpdate()
Model.replaceOne()
Model.updateMany()
Model.updateOne()
自定义方法
- 静态方法
AuthorSchema.statics.findAuthorByName = function (name) {
return new Promise((resolve, reject) => {
this.find({ name }, function (err, doc) {
if (err) return reject(err);
return resolve(doc);
});
});
};
- 实例方法
AuthorSchema.methods.print = function () {
console.log(this);
};
插入数据
mongoose中插入数据的方法有create
、save
。
router.post("/", (req, res) => {
const author = new Author({
name: randomStr(),
});
author.save((err, author) => {
if (err) return res.status(500).send({ msg: "创建失败" });
res.send({ msg: "创建成功", author });
});
});
使用es6语法
router.post("/", async (req, res) => {
const author = new Author({
name: randomStr(),
});
try {
const newAuthor = await author.save();
res.send({ msg: "创建成功", newAuthor });
} catch (error) {
res.status(500).send({ msg: "创建失败" });
}
});
create
将创建和保存一次性完成
router.post("/", async (req, res) => {
try {
const author = await Author.create(req.body);
res.send({ msg: "创建成功", author });
} catch (error) {
res.status(500).send({ msg: "创建失败" });
}
});
查询数据
mongoose中查询数据的方法有find
、findOne
、findById
、findOne
。sort
用于排序,其中-1
表示降序,1
表示升序。skip
跳过指定数量的数据,必须是整数,limit
限制返回条数,也必须是整数。
router.get("/", validator.query, async (req, res) => {
try {
const { pagenum, pagesize, query } = req.query;
const reg = new RegExp(query, "gi");
const authors = await req.Model.find({ name: { $regex: reg } })
.sort({
age: -1,
})
.skip((pagenum - 1) * pagesize)
.limit(parseInt(pagesize));
res.send(authors);
} catch (error) {
res.status(500).send({ msg: "查询失败", error });
}
});
修改数据
mongoose中更新数据的方法有findByIdAndUpdate
、findOneAndUpdate
、findOneAndReplace
、replaceOne
、updateOne
、updateMany
。
router.put("/:id", async (req, res) => {
try {
const updateResult = await Author.findByIdAndUpdate(
req.params.id,
req.body
);
res.send({ msg: "修改成功", author: updateResult });
} catch (error) {
console.log(error);
res.status(500).send({ msg: "修改失败" });
}
});
删除数据
mongoose中删除数据的方法有findByIdAndDelete
、findByIdAndRemove
、findOneAndDelete
、findOneAndRemove
、deleteOne
、deleteMany
。
router.delete("/:id", async (req, res) => {
try {
await Author.findByIdAndDelete(req.params.id);
res.send({ msg: "删除成功" });
} catch (error) {
console.log(error);
res.status(500).send({ msg: "删除失败" });
}
});
填充populate
在需要连表查询的时候,可以使用mongo自带的$lookup
,然而,在mongoose中,我们还可以使用更加强大的populate
,它可以将某些字段替换为从其他 collection 获取的 document
建立依赖关系
models/Person.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const PersonSchema = Schema({
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: "Story" }],
});
module.exports = mongoose.model("Person", PersonSchema);
models/Story.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const StorySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: "Person" },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: "Person" }],
});
module.exports = mongoose.model("Story", StorySchema);
现在我们创建了两个model,其中Person模型中的stories
字段是ObjectId
构成的数组(而且必须是Story
模型中存在的ObjectId
),ref
选项指定populate
时需要用哪个模型进行填充
保存refs
保存 refs 和保存普通属性相同,将ref指定模型的实例id填入该字段即可
populate
const express = require("express");
const router = express.Router();
const Story = require("../models/Story");
const Person = require("../models/Person");
router.get("/getStories", async (req, res) => {
// 由于需要连表查询,如果没有 require Person 模型,会报 Schema hasn't been registered for model "Person". 错误,建议在建立数据库连接之后,通过 require-all 将所有模型导入进来
const result = await Story.find({}).populate("author");
res.send(result);
});
router.get("/getPeople", async (req, res) => {
const result = await Person.find({}).populate("stories");
res.send(result);
});
module.exports = router;
getStories
返回结果为
[
{
"fans": [],
"_id": "5f56df7741a2f336a6233779",
"author": {
"stories": [],
"_id": "5f56de5cad10c13685f0fc39",
"name": "张三",
"age": 40,
"__v": 0
},
"title": "龟兔赛跑",
"__v": 0
},
{
"fans": [],
"_id": "5f56df8441a2f336a623377a",
"author": {
"stories": [],
"_id": "5f56de5cad10c13685f0fc39",
"name": "张三",
"age": 40,
"__v": 0
},
"title": "田忌赛马",
"__v": 0
},
{
"fans": [],
"_id": "5f56df9441a2f336a623377b",
"author": {
"stories": [],
"_id": "5f56de5cad10c13685f0fc39",
"name": "张三",
"age": 40,
"__v": 0
},
"title": "武松打虎",
"__v": 0
}
]
getPeople
返回的结果为
[
{
"stories": [],
"_id": "5f56de5cad10c13685f0fc39",
"name": "张三",
"age": 40,
"__v": 0
},
{
"stories": [],
"_id": "5f56de6ead10c13685f0fc3a",
"name": "李四",
"age": 45,
"__v": 0
}
]
经过对比我们发现,通过Person
对象无法返回stories
列表,原因是我们在创建Story时候为author字段指定了一个Person的ObjectId,但是在创建Person时候,没有向stories
数组中push任何内容。一种解决方案是在创建Person对象时同时指定story,另一种方案是通过Person的id查询该Person的所有story
填充文档的一部分
如果我们只希望获得填充文档的某些特定的字段,可以通过populate
的第二个参数来设置,它是一个字符串类型,不同字段用空格分隔,_id
默认返回,可以使用-_id
排除它
router.get("/getStories", async (req, res) => {
const result = await Story.find({}).populate("author", "name -_id");
res.send(result);
});
填充多个字段
通过对同一个模型多次调用popualte
来填充不同字段
Story.
find({}).
populate('fans').
populate('author').
exec();
如果对同一个路径多次调用populate
,只有最后一次才会生效
Story.
find().
populate({ path: 'fans', select: 'name' }).
populate({ path: 'fans', select: 'email' });
// The above is equivalent to:
Story.find().populate({ path: 'fans', select: 'email' });
查询条件
match
可以对填充的文档进行筛选,只有符合条件的才会被填充。
router.get("/getStories", async (req, res) => {
const result = await Story.find({}).populate({
path: "author",
match: { age: { $gte: 40 } },
});
res.send(result);
});
限制填充的数量
perDocumentLimit
可以对填充文档的数量进行限制
const stories = await Story.find().sort({ name: 1 }).populate({
path: 'fans',
// Special option that tells Mongoose to execute a separate query
// for each `story` to make sure we get 2 fans for each story.
perDocumentLimit: 2
});
stories[0].fans.length; // 2
stories[1].fans.length; // 2
上述查询语句限制了每个story最多返回两个粉丝的数据
多级填充
假设User表有一个字段friends
关联到自身,
const UserSchema = new Schema({
name: String,
friends: [{ type: ObjectId, ref: 'User' }]
});
module.exports = mongoose.model('User', UserSchema);
显然,我们可以通过populate获得用户的朋友,但如果想获得用户朋友的朋友呢?只需要指定populate
就可以了
User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
// Get friends of friends - populate the 'friends' array for every friend
populate: { path: 'friends' }
});
虚拟值填充
populate
不仅可以基于ObjectId进行填充,也可以基于其他字段进行填充,这个时候需要用到virtual
const PersonSchema = new Schema({
name: String,
band: String
});
const BandSchema = new Schema({
name: String
});
BandSchema.virtual('members', {
ref: 'Person', // 需要关联查询的模型
localField: 'name', // 查找 localField 等于 foreignField 的 Person
foreignField: 'band',
// justOne 为 true 时 members将是一个document而不是document构成的数组,默认为 false
justOne: false
});
var Person = mongoose.model('Person', PersonSchema);
var Band = mongoose.model('Band', BandSchema);
/**
* Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue"
* And 4 people: "Axl Rose" and "Slash" with "Guns N' Roses", and
* "Vince Neil" and "Nikki Sixx" with "Motley Crue"
*/
Band.find({}).populate('members').exec(function(error, bands) {
/* `bands.members` is now an array of instances of `Person` */
});
如果希望虚拟值可以被toJSON()
输出,需要在定义Schema时设置virtuals: true
const BandSchema = new Schema({
name: String
}, { toJSON: { virtuals: true } });
如果只希望获得填充数据的一部分,要确保foreignField
包含在select
中
BandSchema.virtual('members', {
ref: 'Person', // 需要关联查询的模型
localField: 'name', // 查找 localField 等于 foreignField 的 Person
foreignField: 'band',
// justOne 为 true 时 members将是一个document而不是document构成的数组,默认为 false
justOne: false
});
Band.
find({}).
populate({ path: 'members', select: 'name' }).
exec(function(error, bands) {
// 查询失败,因为 foreignField 不再查询范围中
});
Band.
find({}).
populate({ path: 'members', select: 'name band' }).
exec(function(error, bands) {
// 查询成功
});
聚合管道aggregate
aggregate
是mongodb的语法,也可以在mongoose
中使用。
聚合表达式
一般用于对文档进行统计、计算,常见的聚合表达式有
- $sum 计算和
- $avg 计算平均值
- $min 最小值
- $max 最大值
管道
- $project:修改文档结构,添加和删除字段
- $match:过滤数据,只输出符合条件的文档
- $limit:限制返回文档数量
- $skip:跳过指定文档
- $unwind:按文档中的某一个数组类型字段的长度复制多份,每份含有数组中的一个值
- $group:分组
- $sort:排序