Nodejs基础3

Mongo - Data Validation

Validation

1
2
3
4
5
6
7
8
const courseSchema = new mongoose.Schema({
name: String,
author: String,
tags: [ String ],
date: Date,
isPublished: Boolean,
price: Number
});

之前我们对于表的定义是这样的,所以当我创建一个空对象也是可以通过验证的。MongoDB不会在意我们的课程是否有单价。所以我们需要required验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const courseSchema = new mongoose.Schema({
name: {type: String,required: true},
author: String,
tags: [ String ],
date: Date,
isPublished: Boolean,
price: Number
});
//...
try {
const result = await course.save();
console.log(result);
} catch (ex) {
console.log(ex.message);
}
//...

这时候当我们去掉name信息之后,运行createCourse()函数,catch会捕捉到下面的报错

Course validation failed: name: Path name is required.

我们也可以手工处理验证

1
await course.validate();

不想Mysql,mongodb是没有必须填写这种验证的。所以验证只有写在mongoose里面才会起作用。当我们尝试写入数据库时,mongoose会做验证,不通过就不会向数据库写入

当然我们可以使用Joi来验证字符串,一般在RESTful API种使用Joi. Mongoose在写入数据库之前验证数据是否合法

因为可能客户端将合法的数据写在了请求当中,但是当我们在创建http 服务的course对象的时候,也许我们忘记把name属性从req.body.name中读取出来了。所以利用mongoose可以确保数据库中不会出现不合法的数据文档

Built-in Validators

我们现在假设,price只有当isPublished为true得时候才是必需的。现在我们来写这个验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const courseSchema = new mongoose.Schema({
name: {type: String,required: true},
author: String,
tags: [ String ],
date: Date,
isPublished: Boolean,
price: {
type:Number,
required: function(){ return this.isPublished;}
}
});
const Course = mongoose.model("Course", courseSchema);
async function createCourse() {
const course = new Course({
// name: "Node.js Course,
author: "Mosh",
tags: ["node", "backend"],
isPublished: true,
});
try {
const result = await course.save();
console.log(result);
} catch (ex) {
console.log(ex.message);
}
}

注意,在这个特定的地方是不能用箭头函数的,因为箭头函数没有this,它使用的this是继承而来的。所以箭头函数中的this指的是courseSchema。

运行后,我们发现

Course validation failed: price: Path price is required., name: Path name is required.

所以验证既可以是一个简单的值,也可以是一个验证条件的函数

当然我们也可以有附加的验证器

比如,在string类型中

1
2
3
4
5
6
7
name: {
type: String,
required: true,
minlenght: 5,
maxlenght: 255,
match: /pattern/ //写正则表达式
},

我们还可以用enum属性(python中的数组).也就是说,我们添加的新的内容,必须在enum给出的范围之内

1
2
3
4
5
category:{
type: String,
required: true,
enum: ['web','mobile','network']
}

对于数字,我们可以有min 和max

1
2
3
4
5
6
price: {
type:Number,
required: function(){ return this.isPublished;}
min: 10,
max: 200
}

Custom Validators

有时候内建的验证器并不能满足我们的需求

例如tags属性,他是一个字符串数组,假设我们要求每个课程必须有一个tag,那么我们在这里无法使用required。因为当使用了required,传入一个空数组,mongoose也会认为这是合法的。所以我们要自定义验证器了

我们设置validate属性是一个对象,这个对象中有个属性是validator,这是一个函数,函数里面写验证逻辑

此外还需要一条提示信息。

1
2
3
4
5
6
7
8
9
tags:{
type: Array,
validate:{
validator: function(v){
return v&&v.length>0;
},
message:"A course must have one tag" //提示信息是可选的
}
}

现在当tags数组为空,tags: null 或者不传入tags的时候,就会出现以下报错:

Course validation failed: price: Path price is required., name: Path name is required., tags: A course must have one tag

Async Validators

验证有时候需要读取数据库或者远端的http服务,我们不能直接得到结论。这时候需要异步验证

1
2
3
4
5
6
7
8
9
10
11
12
13
tags:{
type: Array,
validate:{
isAsync: true,
validator: function(v,callback){
setTimeout(()=>{
const result = v&&v.length>0;
callback(result);
},4000);
},
message:"A course must have one tag"
}
}

首先设置isAsync 属性为true,其次这一个回调函数。这里用setTimeout()代替

现实项目当中这个结果可能来自于对文件系统,数据库或者远端服务返回值的计算

Validation Errors

现在我们来扩展Error对象的细节

1
2
3
4
5
6
7
try {
const result = await course.save();
console.log(result);
} catch (ex) {
for(field in ex.errors)
console.log(ex.errors[field]);
}

我们可以看到这里 一开始是验证错误的对象,然后是错误信息的提示。然后是堆栈的追踪信息。

SchemaType Options

当定义schema的时候,可以直接定义属性的类型,也可以使用对象:type,required,enum等等属性。

现在我们再来了解几个有用的schema对象的属性

lowercase: 当lowercase is true时,mongoose会自动将字符串转为小写

uppercase: 当uppercase is true时,mongoose会自动将字符串转为大写

自定义get,set。比如我想要对price数值四舍五入小数部分。

这样当写入price的时候,set属性会自动四舍五入

get的作用是在数据库中取得浮点数的时候会自动以四舍五入的方式呈现(数据库中没有改变)

1
2
3
4
price:{
get: v=>Math.round(v),
set: v=>Math.round(v)
}

Project- Add Persistence to Genres API

genre.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const Joi = require("joi");
const mongoose = require("mongoose");
const express = require("express");
const router = express.Router();

const Genre = mongoose.model(
"Genre",
new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 50,
},
})
);

router.get("/", async (req, res) => {
const genres = await Genre.find().sort("name");
res.send(genres);
});

router.post("/", async (req, res) => {
const { error } = validateGenre(req.body);
if (error) return res.status(400).send(error.details[0].message);

let genre = new Genre({ name: req.body.name });
genre = await genre.save();

res.send(genre);
});

router.put("/:id", async (req, res) => {
const { error } = validateGenre(req.body);
if (error) return res.status(400).send(error.details[0].message);

const genre = await Genre.findByIdAndUpdate(
req.params.id,
{ name: req.body.name },
{
new: true,
}
);

if (!genre)
return res.status(404).send("The genre with the given ID was not found.");

res.send(genre);
});

router.delete("/:id", async (req, res) => {
const genre = await Genre.findByIdAndRemove(req.params.id);

if (!genre)
return res.status(404).send("The genre with the given ID was not found.");

res.send(genre);
});

router.get("/:id", async (req, res) => {
const genre = await Genre.findById(req.params.id);

if (!genre)
return res.status(404).send("The genre with the given ID was not found.");

res.send(genre);
});

function validateGenre(genre) {
const schema = {
name: Joi.string().min(3).required(),
};

return Joi.validate(genre, schema);
}

module.exports = router;

index.js

1
2
3
4
5
6
7
8
9
10
11
const mongoose = require('mongoose')
const express = require("express");
const app = express();
const genres = require("./routes/genres");
app.use(express.json());
mongoose.connect('mongodb://localhost/vidly')
.then(()=>console.log("Connected to MongoDB"))
.catch(err => console.error('Could not connect to MongoDB'))
app.use("/api/genres", genres);
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on port ${port}...`));

Project- Build the Customers API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const Joi = require('joi');
const mongoose = require('mongoose');
const express = require('express');
const router = express.Router();

const Customer = mongoose.model('Customer', new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 50
},
isGold: {
type: Boolean,
default: false
},
phone: {
type: String,
required: true,
minlength: 5,
maxlength: 50
}
}));

router.get('/', async (req, res) => {
const customers = await Customer.find().sort('name');
res.send(customers);
});

router.post('/', async (req, res) => {
const { error } = validateCustomer(req.body);
if (error) return res.status(400).send(error.details[0].message);

let customer = new Customer({
name: req.body.name,
isGold: req.body.isGold,
phone: req.body.phone
});
customer = await customer.save();

res.send(customer);
});

router.put('/:id', async (req, res) => {
const { error } = validateCustomer(req.body);
if (error) return res.status(400).send(error.details[0].message);

const customer = await Customer.findByIdAndUpdate(req.params.id,
{
name: req.body.name,
isGold: req.body.isGold,
phone: req.body.phone
}, { new: true });

if (!customer) return res.status(404).send('The customer with the given ID was not found.');

res.send(customer);
});

router.delete('/:id', async (req, res) => {
const customer = await Customer.findByIdAndRemove(req.params.id);

if (!customer) return res.status(404).send('The customer with the given ID was not found.');

res.send(customer);
});

router.get('/:id', async (req, res) => {
const customer = await Customer.findById(req.params.id);

if (!customer) return res.status(404).send('The customer with the given ID was not found.');

res.send(customer);
});

function validateCustomer(customer) {
const schema = {
name: Joi.string().min(5).max(50).required(),
phone: Joi.string().min(5).max(50).required(),
isGold: Joi.boolean()
};

return Joi.validate(customer, schema);
}

module.exports = router;

Restructuring the Project

现在一个js文件虽然只有80多行,但以后肯定时很庞大的,所以我们要进行代码重构。

所以我们把一些对象都放到models文件夹中,routes专心处理api

在models中我们新建一个customer.js存放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const mongoose = require("mongoose");
const Joi = require("joi");

const Customer = mongoose.model(
"Customer",
new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 50,
},
isGold: {
type: Boolean,
default: false,
},
phone: {
type: String,
required: true,
minlength: 5,
maxlength: 50,
},
})
);
function validateCustomer(customer) {
const schema = {
name: Joi.string().min(5).max(50).required(),
phone: Joi.string().min(5).max(50).required(),
isGold: Joi.boolean(),
};

return Joi.validate(customer, schema);
}
exports.Customer = Customer;
exports.valudate = validateCustomer;

这样我们实现了 single responsibility principle

在customer model中,我们实现了对customer对象的定义和验证。也就是定义了在node中一个customer对象应该长什么样子

routes 中的customer.js 就是全部处理关于customer的路由逻辑

在最后我们导出,并在routes中接收。注意,我们这里用析构而不直接定义,是因为我们不想这样写:customer.Customer,太难看了

const {Customer,validate}= require(‘../model/customer’);

同样的我们对genre.js进行重构

Mongoose Validation Recap

Mongoose- Modeling Relationships between Connected Data

Modelling Relationships

Referencing Documents

Population

Embedding Documents

Using an Array of Sub-documents

Project- Build the Movies API

Project- Build the Rentals API

Mongoose- Modelling Relationships between Connected Data Recap

-------------本文结束,感谢您的阅读-------------