Sequelize 数据验证与约束 - 保证数据质量的最佳实践
发布时间:2024-03-05
作者:一介布衣
标签:Sequelize, 数据验证, 约束, 数据质量
前言
上一篇文章我们学习了关联关系,今天咱们来学习数据验证和约束。说实话,数据验证是保证应用数据质量的第一道防线,也是最重要的一道防线。
我记得刚开始做项目的时候,总是觉得前端验证就够了,后端验证是多余的。结果线上经常出现脏数据,用户直接调用 API 绕过前端验证,或者前端验证被篡改,导致各种奇怪的问题。后来才明白,后端验证是必不可少的。
今天我就把 Sequelize 的验证和约束功能详细讲解一下,让大家能够构建健壮的数据层。
验证 vs 约束
在开始之前,我们先理解验证和约束的区别:
- 验证(Validations):在 JavaScript 层面进行的检查,发生在数据保存到数据库之前
- 约束(Constraints):在数据库层面进行的检查,由数据库引擎强制执行
两者结合使用,能够提供最全面的数据保护。
内置验证器
1. 基本验证器
javascript
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: '邮箱不能为空'
},
notEmpty: {
msg: '邮箱不能为空字符串'
},
isEmail: {
msg: '请输入有效的邮箱地址'
}
}
},
age: {
type: DataTypes.INTEGER,
validate: {
isInt: {
msg: '年龄必须是整数'
},
min: {
args: [0],
msg: '年龄不能小于0'
},
max: {
args: [120],
msg: '年龄不能大于120'
}
}
},
username: {
type: DataTypes.STRING,
validate: {
len: {
args: [3, 20],
msg: '用户名长度必须在3-20个字符之间'
},
isAlphanumeric: {
msg: '用户名只能包含字母和数字'
},
notIn: {
args: [['admin', 'root', 'system']],
msg: '用户名不能是保留字'
}
}
},
website: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isUrl: {
msg: '请输入有效的网址'
}
}
},
phone: {
type: DataTypes.STRING,
validate: {
is: {
args: /^1[3-9]\d{9}$/,
msg: '请输入有效的手机号码'
}
}
}
});
2. 数值验证器
javascript
const Product = sequelize.define('Product', {
price: {
type: DataTypes.DECIMAL(10, 2),
validate: {
isDecimal: {
msg: '价格必须是有效的小数'
},
min: {
args: [0],
msg: '价格不能为负数'
}
}
},
stock: {
type: DataTypes.INTEGER,
validate: {
isInt: true,
min: 0
}
},
rating: {
type: DataTypes.FLOAT,
validate: {
isFloat: {
min: 0,
max: 5,
msg: '评分必须在0-5之间'
}
}
}
});
3. 字符串验证器
javascript
const Article = sequelize.define('Article', {
title: {
type: DataTypes.STRING,
validate: {
len: {
args: [5, 100],
msg: '标题长度必须在5-100个字符之间'
},
notEmpty: true
}
},
slug: {
type: DataTypes.STRING,
validate: {
is: {
args: /^[a-z0-9-]+$/,
msg: 'URL别名只能包含小写字母、数字和连字符'
}
}
},
content: {
type: DataTypes.TEXT,
validate: {
len: {
args: [10, 10000],
msg: '内容长度必须在10-10000个字符之间'
}
}
},
status: {
type: DataTypes.ENUM('draft', 'published', 'archived'),
validate: {
isIn: {
args: [['draft', 'published', 'archived']],
msg: '状态值无效'
}
}
}
});
4. 日期验证器
javascript
const Event = sequelize.define('Event', {
startDate: {
type: DataTypes.DATE,
validate: {
isDate: {
msg: '开始日期格式无效'
},
isAfter: {
args: new Date().toISOString(),
msg: '开始日期不能早于今天'
}
}
},
endDate: {
type: DataTypes.DATE,
validate: {
isDate: true,
isAfterStartDate(value) {
if (value <= this.startDate) {
throw new Error('结束日期必须晚于开始日期');
}
}
}
},
birthDate: {
type: DataTypes.DATEONLY,
validate: {
isBefore: {
args: new Date().toISOString().split('T')[0],
msg: '出生日期不能是未来日期'
}
}
}
});
自定义验证器
1. 简单自定义验证
javascript
const User = sequelize.define('User', {
password: {
type: DataTypes.STRING,
validate: {
// 密码强度验证
isStrongPassword(value) {
const strongRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])/;
if (!strongRegex.test(value)) {
throw new Error('密码必须包含大小写字母、数字和特殊字符');
}
if (value.length < 8) {
throw new Error('密码长度至少8位');
}
},
// 不能包含用户名
notContainUsername(value) {
if (this.username && value.toLowerCase().includes(this.username.toLowerCase())) {
throw new Error('密码不能包含用户名');
}
}
}
},
idCard: {
type: DataTypes.STRING,
validate: {
// 身份证号验证
isValidIdCard(value) {
if (!value) return;
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
if (!idCardRegex.test(value)) {
throw new Error('身份证号格式不正确');
}
// 校验位验证
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(value[i]) * weights[i];
}
const checkCode = checkCodes[sum % 11];
if (value[17].toUpperCase() !== checkCode) {
throw new Error('身份证号校验位不正确');
}
}
}
}
});
2. 异步验证器
javascript
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
validate: {
// 异步验证邮箱唯一性
async isUniqueEmail(value) {
const existingUser = await User.findOne({
where: {
email: value,
id: { [Op.ne]: this.id || 0 } // 排除自己
}
});
if (existingUser) {
throw new Error('邮箱已被使用');
}
},
// 验证邮箱域名
async isAllowedDomain(value) {
const allowedDomains = ['company.com', 'partner.com'];
const domain = value.split('@')[1];
if (!allowedDomains.includes(domain)) {
throw new Error('只允许公司邮箱注册');
}
}
}
},
username: {
type: DataTypes.STRING,
validate: {
// 检查用户名是否被禁用
async isNotBanned(value) {
const bannedUsernames = await BannedUsername.findAll({
where: { username: value },
raw: true
});
if (bannedUsernames.length > 0) {
throw new Error('该用户名已被禁用');
}
}
}
}
});
3. 条件验证器
javascript
const User = sequelize.define('User', {
userType: {
type: DataTypes.ENUM('individual', 'company'),
allowNull: false
},
companyName: {
type: DataTypes.STRING,
validate: {
// 只有企业用户需要填写公司名称
requiredForCompany(value) {
if (this.userType === 'company' && !value) {
throw new Error('企业用户必须填写公司名称');
}
}
}
},
taxNumber: {
type: DataTypes.STRING,
validate: {
// 企业用户的税号验证
validTaxNumber(value) {
if (this.userType === 'company') {
if (!value) {
throw new Error('企业用户必须填写税号');
}
// 统一社会信用代码验证
const taxRegex = /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/;
if (!taxRegex.test(value)) {
throw new Error('税号格式不正确');
}
}
}
}
}
});
模型级验证
1. 跨字段验证
javascript
const User = sequelize.define('User', {
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
fullName: DataTypes.STRING,
password: DataTypes.STRING,
confirmPassword: DataTypes.VIRTUAL // 虚拟字段,不存储到数据库
}, {
validate: {
// 姓名一致性验证
nameConsistency() {
if (this.fullName && this.firstName && this.lastName) {
const expectedFullName = `\${this.firstName} \${this.lastName}`;
if (this.fullName !== expectedFullName) {
throw new Error('全名与姓氏不匹配');
}
}
},
// 密码确认验证
passwordMatch() {
if (this.password && this.confirmPassword) {
if (this.password !== this.confirmPassword) {
throw new Error('两次输入的密码不一致');
}
}
},
// 至少填写一个联系方式
hasContact() {
if (!this.email && !this.phone) {
throw new Error('至少需要填写邮箱或手机号');
}
}
}
});
2. 业务逻辑验证
javascript
const Order = sequelize.define('Order', {
userId: DataTypes.INTEGER,
productId: DataTypes.INTEGER,
quantity: DataTypes.INTEGER,
totalAmount: DataTypes.DECIMAL(10, 2),
status: DataTypes.ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled')
}, {
validate: {
// 订单金额验证
async validOrderAmount() {
if (this.productId && this.quantity) {
const product = await Product.findByPk(this.productId);
if (product) {
const expectedAmount = product.price * this.quantity;
if (Math.abs(this.totalAmount - expectedAmount) > 0.01) {
throw new Error('订单金额计算错误');
}
}
}
},
// 库存验证
async checkStock() {
if (this.productId && this.quantity) {
const product = await Product.findByPk(this.productId);
if (product && product.stock < this.quantity) {
throw new Error('库存不足');
}
}
},
// 状态变更验证
validStatusTransition() {
if (!this.isNewRecord && this.changed('status')) {
const validTransitions = {
'pending': ['paid', 'cancelled'],
'paid': ['shipped', 'cancelled'],
'shipped': ['completed'],
'completed': [],
'cancelled': []
};
const previousStatus = this.previous('status');
const newStatus = this.status;
if (!validTransitions[previousStatus]?.includes(newStatus)) {
throw new Error(`不能从 \${previousStatus} 状态变更为 \${newStatus} 状态`);
}
}
}
}
});
数据库约束
1. 基本约束
javascript
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
email: {
type: DataTypes.STRING,
allowNull: false, // NOT NULL 约束
unique: true, // UNIQUE 约束
validate: {
isEmail: true
}
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: {
name: 'username_unique',
msg: '用户名已存在'
}
},
age: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
min: 0,
max: 120
}
}
});
2. 外键约束
javascript
const Post = sequelize.define('Post', {
title: DataTypes.STRING,
content: DataTypes.TEXT,
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id'
},
onUpdate: 'CASCADE', // 更新时级联
onDelete: 'RESTRICT' // 删除时限制
},
categoryId: {
type: DataTypes.INTEGER,
references: {
model: 'categories',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL' // 删除时设为空
}
});
3. 检查约束
javascript
// 注意:检查约束不是所有数据库都支持
const Product = sequelize.define('Product', {
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
validate: {
min: 0
}
},
discountPrice: {
type: DataTypes.DECIMAL(10, 2),
validate: {
isLessThanPrice(value) {
if (value && value >= this.price) {
throw new Error('折扣价必须小于原价');
}
}
}
}
}, {
// 表级检查约束(PostgreSQL)
indexes: [
{
fields: ['price', 'discountPrice'],
where: {
discountPrice: {
[Op.lt]: sequelize.col('price')
}
}
}
]
});
验证错误处理
1. 捕获验证错误
javascript
async function createUser(userData) {
try {
const user = await User.create(userData);
return { success: true, user };
} catch (error) {
if (error.name === 'SequelizeValidationError') {
// 验证错误
const errors = error.errors.map(err => ({
field: err.path,
message: err.message,
value: err.value
}));
return {
success: false,
type: 'validation',
errors
};
} else if (error.name === 'SequelizeUniqueConstraintError') {
// 唯一约束错误
const field = error.errors[0].path;
return {
success: false,
type: 'unique',
message: `\${field} 已存在`,
field
};
} else if (error.name === 'SequelizeForeignKeyConstraintError') {
// 外键约束错误
return {
success: false,
type: 'foreign_key',
message: '关联数据不存在'
};
} else {
// 其他错误
console.error('创建用户失败:', error);
return {
success: false,
type: 'unknown',
message: '服务器内部错误'
};
}
}
}
2. 自定义错误处理
javascript
class ValidationError extends Error {
constructor(errors) {
super('Validation failed');
this.name = 'ValidationError';
this.errors = errors;
}
toJSON() {
return {
name: this.name,
message: this.message,
errors: this.errors
};
}
}
// 统一验证函数
async function validateAndCreate(Model, data) {
try {
// 先进行验证,不保存
const instance = Model.build(data);
await instance.validate();
// 验证通过,保存到数据库
return await instance.save();
} catch (error) {
if (error.name === 'SequelizeValidationError') {
const formattedErrors = error.errors.reduce((acc, err) => {
acc[err.path] = err.message;
return acc;
}, {});
throw new ValidationError(formattedErrors);
}
throw error;
}
}
性能优化
1. 验证优化
javascript
// 跳过验证(谨慎使用)
const user = await User.create(userData, {
validate: false // 跳过验证,提高性能
});
// 只验证特定字段
await user.save({
fields: ['name', 'email'], // 只保存和验证这些字段
validate: true
});
// 批量操作时的验证
await User.bulkCreate(userList, {
validate: true, // 启用验证
individualHooks: true // 启用单个钩子
});
2. 约束优化
javascript
// 延迟约束检查(PostgreSQL)
const transaction = await sequelize.transaction({
deferrable: Sequelize.Deferrable.SET_DEFERRED
});
try {
// 在事务中进行可能违反约束的操作
await User.create(userData, { transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
实战案例
用户注册验证
javascript
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
validate: {
len: [3, 50],
isAlphanumeric: true,
async isAvailable(value) {
const existing = await User.findOne({ where: { username: value } });
if (existing) {
throw new Error('用户名已被使用');
}
}
}
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true,
async isUniqueEmail(value) {
const existing = await User.findOne({ where: { email: value } });
if (existing) {
throw new Error('邮箱已被注册');
}
}
}
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [8, 100],
isStrongPassword(value) {
const strongRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/;
if (!strongRegex.test(value)) {
throw new Error('密码必须包含大小写字母、数字和特殊字符');
}
}
}
},
phone: {
type: DataTypes.STRING(20),
validate: {
is: /^1[3-9]\d{9}$/,
async isUniquePhone(value) {
if (value) {
const existing = await User.findOne({ where: { phone: value } });
if (existing) {
throw new Error('手机号已被注册');
}
}
}
}
}
}, {
validate: {
// 至少填写邮箱或手机号
hasContactInfo() {
if (!this.email && !this.phone) {
throw new Error('请至少填写邮箱或手机号');
}
}
}
});
// 注册服务
class UserService {
static async register(userData) {
try {
const user = await User.create(userData);
return { success: true, user: user.toSafeObject() };
} catch (error) {
return this.handleValidationError(error);
}
}
static handleValidationError(error) {
if (error.name === 'SequelizeValidationError') {
const errors = {};
error.errors.forEach(err => {
errors[err.path] = err.message;
});
return {
success: false,
type: 'validation',
errors
};
}
if (error.name === 'SequelizeUniqueConstraintError') {
const field = error.errors[0].path;
return {
success: false,
type: 'unique',
message: `\${field} 已存在`
};
}
throw error;
}
}
总结
今天我们深入学习了 Sequelize 的数据验证和约束:
- ✅ 内置验证器的使用方法
- ✅ 自定义验证器的编写技巧
- ✅ 模型级验证和业务逻辑验证
- ✅ 数据库约束的配置
- ✅ 验证错误的处理方式
- ✅ 性能优化和实战案例
掌握了这些知识,你就能够:
- 构建健壮的数据验证体系
- 保证应用数据的质量和一致性
- 处理各种验证错误场景
- 优化验证性能
下一篇文章,我们将学习 Sequelize 的事务处理,这是保证数据一致性的重要机制。
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!