Mongoose基本用法

安装依赖
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方法
内置方法
自定义方法
  • 静态方法
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中插入数据的方法有createsave

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中查询数据的方法有findfindOnefindByIdfindOnesort用于排序,其中-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中更新数据的方法有findByIdAndUpdatefindOneAndUpdatefindOneAndReplacereplaceOneupdateOneupdateMany

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中删除数据的方法有findByIdAndDeletefindByIdAndRemovefindOneAndDeletefindOneAndRemovedeleteOnedeleteMany

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:排序
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。