mongoose笔记

mongoose 笔记

实践开发过程中总结的一些笔记。

参考

连接数据库

  • 配置文件 config.js
// 数据库地址: 'mongodb://用户名:密码@ip地址:端口号/数据库';
// 生产环境数据库地址放到进程环境 process.env.MONGOFB_URL
module.exports = {
  mongodb: "mongodb://127.0.0.1:27017/my_library",
};
  • 连接
const config = require("./config");
const mongoose = require("mongoose");
const db_url = process.env.MONGOFB_URL || config.mongodb;

module.exports = ()=>{
    mongoose.connect(db_url, { useNewUrlParser: true, useUnifiedTopology: true });
    const db = mongoose.connection;
    db.on("error", console.error.bind(console, "MongoDB 连接错误:"));
    db.once("connected", () => console.log("MongoDB连接成功!"));

    return db
}

常见字段类型和声明方式

const schema = new Schema(
{
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65, required: true },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  array: [],
  ofString: [String], // 其他类型也可使用数组
  nested: { stuff: { type: String, lowercase: true, trim: true } }
})

自定义的验证器

内置的验证器包括:

  • 所有 模式类型 都具有内置的 required 验证器。用于指定当前字段是否为保存文档所必需的。
  • Number 有数值范围验证器 min 和 max。
  • String 有:
    • enum:指定当前字段允许值的集合。
    • match:指定字符串必须匹配的正则表达式。
    • 字符串的最大长度 maxlength 和最小长度 minlength
const breakfastSchema = new Schema({
  name: {
  	type: String, 
    required: true
  },
  eggs: {
    type: Number,
    min: [6, '鸡蛋太少'],
    max: 12
  },
  drink: {
    type: String,
    enum: ['咖啡', '茶'],
    default:'咖啡'
  }
});

虚拟属性

虚拟属性是可以获取和设置、但不会保存到 MongoDB 的文档属性。getter 可用于格式化或组合字段,而 setter 可用于将单个值分解为多个值从而便于存储。文档中的示例,从名字和姓氏字段构造(并解构)一个全名虚拟属性,这比每次在模板中使用全名更简单,更清晰。

官方文档地址virtuals

const mongoose = require("mongoose");

const AuthorSchema = new mongoose.Schema({
  __v: { type: Number, select: false },
  firstName: { type: String, required: true, maxLength: 100 },
  lastName: { type: String, required: true, maxLength: 100 },
  birthDay: { type: Date },
});

// 设置虚拟属性
AuthorSchema.virtual("fullName").get(function () {
  return this.firstName+"-"+this.lastName;
});

// 让虚拟属性可见
AuthorSchema.set('toJSON', {virtuals: true});
module.exports = mongoose.model("Author", AuthorSchema);

虚拟属性virtuals在接口请求的数据中是不会显示的,需要在options中配置{ toJSON: { virtuals: true } },或者在schema实例上设置:

itemSchema.set('toJSON', {
    virtuals: true
});

模拟虚拟属性

这里通过 Object.defineProperty 方法模拟virtuals属性,virtuals设置的值不能被枚举,同时只能通过getter方式获取:

const obj = {};
Object.defineProperty(obj,"firstName",{enumerable: false,get:()=>"chen"});
Object.defineProperty(obj,"lastName",{enumerable: false,get:()=>"wl"});

Object.defineProperty(obj, "fullName", {
    enumerable: false,
    get(){
        return this.firstName+"-"+this.lastName
    }
});

console.log(obj); //author_find.html:26 {firstName: "chen", lastName: "wl"}
console.log(Object.keys(obj)); // []
console.log(obj.fullName); // chen-wl

添加时间戳

增加文档的插入时间戳和更新时间戳 option: timestamps

new Schema({..}, { timestamps: true});

或者单独创建:

new Schema({..}, { timestamps: { createdAt: 'created_at' } });

格式化输出数据

上面有通过toJSON设置虚拟属性,这里常用的还包括 toJSON.transform() 方法,再输出查找的数据之前,对数据做格式化处理:

AuthorSchema.set("toJSON", {
  virtuals: true,
  transform:function(_doc,result){
      delete result.birthDay; 
      delete result.firstName; 
      delete result.lastName; 
      delete result._id; 
      return result;
  }
});

经过transform格式化后,查找的数据如下:

{
  "fullName": "chen-weilong",
  "birthDay_yyyy_mm_dd": "2020-11-20",
  "id": "5fb7698c6352a921a3fcb3de"
}

校验钩子 Hooks

通常在保存数据前会对数据进行校验,这里Schema提供了好用的校验钩子函数 Save/Validate Hooks

例如用户更新密码,需要校验新密码跟旧密码是否相同,接着对用户密码进行加密:

const bcryptjs = require("bcryptjs");
UserSchema.pre("save",async function(next){
	// 如果修改的不是password,直接跳过
    if(!this.isModified("password")) return next();
    this.password = await bcryptjs.hash(this.password,10);
    next();
})

isModified 是文档提供的方法

Schema 静态和原型方法

这里结合上面的校验钩子,做一个用户注册和登录的示例,需求如下:

  • 注册

    • 获取用户名和密码
    • 校验用户名是否已经存在
    • 将用户名和加密后密码存入数据库
  • 登录

    • 获取登录用户名和密码
    • 根据用户名查找用户
    • 查找到用户,校验密码是否准确
    • 校验成功,将用户信息转成token发送

登录注册流程会用到三个依赖包:

bcrypt

bcrypt.js 给用户密码加密,使用如下:

let password = "123456";
let hash = bcrypt.hashSync(password, 10);
// 对比
bcrypt.compareSync(password, hash);

validator

validator 校验数据,例如email的校验:

validator.isEmail('foo@bar.com'); //=> true

jsonwebtoken

jwt生成token数据,使用如下:

const jwt = require("jsonwebtoken");

let authData = {};
// 签名
exports.sign = function (obj) {
  let secretData = { ...authData, ...obj };
  const option = { expiresIn: "90d" };
  return jwt.sign(secretData, process.evn.SECRET, option);
};
// 校验
exports.verify = function (token) {
  return new Promise((resolve) => {
    jwt.verify(token, secret, (err, decoded) => {
      err ? resolve({error:true}) : resolve(decoded);
    });
  });
};

model 层

创建用户schema如下:

const userSchema = new Schema({
  email: {
    type: String,
    trim: true,
    validate: {validator: validator.isEmail},
  },
  username: {
    type: String,
    required: [true, "用户名不能为空"],
    minlength: [4, "用户长度不能小于于4位"],
    maxlength: [10, "用户长度不能大于10位"],
  },
  password: {
    type: String,
    select:false,
    required: [true, "密码不能为空"],
    minlength: [8, "用户长度不能小于于8位"],
  },
});

hook钩子pre-save

保存和更新密码前的钩子函数:

userSchema.pre("save", async function (next) {
//如果修改的不是密码,跳过
  if (!this.isModified("password")) return next();
//给密码加盐
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

静态方法

给schema添加静态方法,注意这里不要用箭头函数,否则this指向会错误,更多配置请查看Schema.prototype.static(),添加login方法如下:

userSchema.static("login", async function (username, password) {
  let user = await this.findOne({ username });
  if (!user) return null;

  return bcrypt.compareSync(password, user.password) ? user : null;
});

实例方法

实例方法主要是为查找到的用户生成token,更多api请查看Schema.prototype.method(),新增getAccessToken如下:

const { sign } = require("./my-jwt");
userSchema.methods.getAccessToken = function () {
  return jwt.sign({ id: this._id });
};

loadClass

如果觉得上面的方法实例这些麻烦,可以直接使用loadClass,示例如下:

const md5 = require('md5');
const userSchema = new Schema({ email: String });
class UserClass {
  // `gravatarImage` becomes a virtual
  get gravatarImage() {
    const hash = md5(this.email.toLowerCase());
    return `https://www.gravatar.com/avatar/${hash}`;
  }

  // `getProfileUrl()` becomes a document method
  getProfileUrl() {
    return `https://mysite.com/${this.email}`;
  }

  // `findByEmail()` becomes a static
  static findByEmail(email) {
    return this.findOne({ email });
  }
}

// `schema` will now have a `gravatarImage` virtual, a `getProfileUrl()` method,
// and a `findByEmail()` static
userSchema.loadClass(UserClass);

controller 层

编辑用户controller层代码如下:

class UserCls {
  async find(req,res){
    let users = await User.find();
    res.json(users);
  }
  async regist(req, res) {
    const { username="", password="", email="" } = req.body;
    // validator 校验,这里省略了..
    let user = await User.findOne({ username });
    if(user) res.error(403,"用户已经存在");
    let result = await User.create({ username, password, email })
    res.success(result);
  }
  async login(req,res) {
    const { username="", password="" } = req.body;
    const user = await User.login(username, password);
    if(!user) return res.error(401, "用户名或密码错误");
    const token = user.getAccessToken();
    res.success(token);
  }
}

module.exports = new UserCls();

关联

关联schema设置:

users: { type: [{ type: Schema.Types.ObjectId, ref: "User" }],select:false},

Populate 关联查询:

await Article.findById(id).populate("users");

增删改查

分页查询

路由:router.get("/",find);

async find(req,res){
  let {offset,limit} = req.query;
  limit = Math.max(limit*1,10);
  offset = Math.max(offset*1,0);
  
  let count = await Plan.count();
  if(!count) return res.success({count,list:[]});

  let list = await Plan.find().skip(offset).limit(limit);
  return res.success({ count, list });
}

根据id查询

路由:router.get("/:id", findById);

async findById(req, res) {
  const { id } = req.params;
  const { fields = "" } = req.query;
  // 选择查询字段
  const selectFields = fields
  .split(";")
  .filter((f) => f)
  .map((f) => " +" + f)
  .join("");
  const result = await Article.findById(id)
  .select(selectFields)
  .populate("users");

  res.success(result);
}

新增

路由:router.post("/add",add);

async add(req, res) {
  // 校验
  req.verifyParams({
  title: { required: true, message: "请输入标题" },
  date: { required: true, message: "日期" },
  status: { required: true, enum:[0,1,2], message: "状态" },
  });

  let result = await Article.create(req.body);
  // await new Article(req.body).save()
  res.success(result);
}

修改

路由:router.put("/:id",update);

async update(req, res) {
  let { id } = req.params;
  let result = await Article.findByIdAndUpdate(id, req.body);
  if (!result) return res.error(404,"Article 不存在");
  res.success(result);
}

删除

路由:router.delete("/:id", del);

async delete(req, res) {
  let { id } = req.params;
  let result = await Article.findByIdAndRemove(id);
  if (!result) return res.error(404,"Article 不存在");
  res.success(result);
}