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);
}