Class Validator
class-validator 是一个用于装饰器和非装饰器环境下的校验库,它允许使用装饰器或手动验证来对类的属性进行校验。这个库是基于 TypeScript 编写的,广泛应用于 Node.js 项目中,尤其是与 TypeScript 和 TypeORM 结合使用时,可以有效地确保数据模型的正确性和一致性。

TypeORM 如何使用验证器?包括 class-validator 的集成和自定义验证器的实现数据验证是应用程序开发中的重要环节,TypeORM 可以与各种验证器库集成,确保数据的完整性和一致性。本文将详细介绍 TypeORM 中如何使用验证器进行数据验证。
## 验证器基础概念
### 什么是验证器
验证器是用于验证数据是否符合特定规则的机制,包括:
* 字段类型验证
* 字段格式验证
* 字段长度验证
* 自定义业务规则验证
* 跨字段验证
### 常用验证器库
* **class-validator**: 最流行的 TypeScript 验证器库
* **class-transformer**: 用于对象转换和验证
* **joi**: 强大的对象模式验证库
* **zod**: TypeScript 优先的模式验证库
## 使用 class-validator
### 安装依赖
```bash
npm install class-validator class-transformer
npm install --save-dev @types/class-transformer
```
### 基本验证示例
```typescript
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeInsert,
BeforeUpdate
} from 'typeorm';
import {
IsEmail,
IsNotEmpty,
IsString,
MinLength,
MaxLength,
IsInt,
Min,
Max,
IsOptional,
IsDateString,
IsEnum,
ValidateIf,
ValidateNested
} from 'class-validator';
import { Type } from 'class-transformer';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@IsNotEmpty({ message: 'Name cannot be empty' })
@IsString({ message: 'Name must be a string' })
@MinLength(2, { message: 'Name must be at least 2 characters' })
@MaxLength(100, { message: 'Name must not exceed 100 characters' })
name: string;
@Column({ unique: true })
@IsEmail({}, { message: 'Invalid email format' })
@IsNotEmpty({ message: 'Email cannot be empty' })
email: string;
@Column({ nullable: true })
@IsOptional()
@MinLength(8, { message: 'Password must be at least 8 characters' })
@MaxLength(100, { message: 'Password must not exceed 100 characters' })
password?: string;
@Column({ type: 'int', nullable: true })
@IsOptional()
@IsInt({ message: 'Age must be an integer' })
@Min(18, { message: 'Age must be at least 18' })
@Max(120, { message: 'Age must not exceed 120' })
age?: number;
@Column({ type: 'enum', enum: ['active', 'inactive', 'suspended'], default: 'active' })
@IsEnum(['active', 'inactive', 'suspended'], { message: 'Invalid status' })
status: string;
@Column({ type: 'date', nullable: true })
@IsOptional()
@IsDateString({}, { message: 'Invalid date format' })
birthDate?: Date;
@BeforeInsert()
@BeforeUpdate()
async validate() {
const errors = await validate(this);
if (errors.length > 0) {
throw new Error(`Validation failed: ${JSON.stringify(errors)}`);
}
}
}
```
## 高级验证
### 自定义验证器
```typescript
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator,
ValidationOptions
} from 'class-validator';
// 自定义验证器:检查用户名是否唯一
@ValidatorConstraint({ name: 'isUsernameUnique', async: true })
export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface {
async validate(username: string) {
// 这里应该查询数据库检查用户名是否唯一
// 示例代码
const userExists = await checkUsernameExists(username);
return !userExists;
}
defaultMessage(args: ValidationArguments) {
return 'Username already exists';
}
}
// 自定义装饰器
export function IsUsernameUnique(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsUsernameUniqueConstraint,
});
};
}
// 使用自定义验证器
@Entity()
export class User {
@Column({ unique: true })
@IsUsernameUnique({ message: 'Username already exists' })
username: string;
}
```
### 条件验证
```typescript
import { ValidateIf } from 'class-validator';
@Entity()
export class User {
@Column()
@IsNotEmpty()
accountType: 'personal' | 'business';
@Column({ nullable: true })
@ValidateIf(o => o.accountType === 'business')
@IsNotEmpty({ message: 'Company name is required for business accounts' })
companyName?: string;
@Column({ nullable: true })
@ValidateIf(o => o.accountType === 'business')
@IsNotEmpty({ message: 'Tax ID is required for business accounts' })
taxId?: string;
@Column({ nullable: true })
@ValidateIf(o => o.accountType === 'personal')
@IsNotEmpty({ message: 'Personal ID is required for personal accounts' })
personalId?: string;
}
```
### 嵌套对象验证
```typescript
import { ValidateNested, Type } from 'class-transformer';
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
class Address {
@IsNotEmpty()
@IsString()
street: string;
@IsNotEmpty()
@IsString()
city: string;
@IsNotEmpty()
@IsString()
zipCode: string;
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@IsNotEmpty()
name: string;
@Column({ type: 'json', nullable: true })
@ValidateIf(o => o.hasAddress)
@ValidateNested()
@Type(() => Address)
address?: Address;
@Column({ default: false })
hasAddress: boolean;
}
```
### 跨字段验证
```typescript
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator,
ValidationOptions,
ValidationArguments
} from 'class-validator';
// 自定义验证器:确认密码匹配
@ValidatorConstraint({ name: 'isPasswordMatching', async: false })
export class IsPasswordMatchingConstraint implements ValidatorConstraintInterface {
validate(password: string, args: ValidationArguments) {
const object = args.object as any;
return password === object.password;
}
defaultMessage(args: ValidationArguments) {
return 'Passwords do not match';
}
}
export function IsPasswordMatching(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsPasswordMatchingConstraint,
});
};
}
@Entity()
export class User {
@Column()
@IsNotEmpty()
@MinLength(8)
password: string;
@Column({ nullable: true })
@IsPasswordMatching({ message: 'Passwords do not match' })
confirmPassword?: string;
}
```
## 验证错误处理
### 验证并获取错误
```typescript
import { validate, ValidationError } from 'class-validator';
async function createUser(userData: Partial<User>) {
const user = new User();
Object.assign(user, userData);
const errors = await validate(user);
if (errors.length > 0) {
// 格式化错误信息
const formattedErrors = this.formatValidationErrors(errors);
throw new Error(`Validation failed: ${JSON.stringify(formattedErrors)}`);
}
// 保存用户
return await userRepository.save(user);
}
function formatValidationErrors(errors: ValidationError[]): any {
const result: any = {};
errors.forEach(error => {
const constraints = error.constraints || {};
result[error.property] = Object.values(constraints).join(', ');
if (error.children && error.children.length > 0) {
result[error.property] = {
...result[error.property],
...this.formatValidationErrors(error.children)
};
}
});
return result;
}
// 使用示例
try {
const user = await createUser({
name: '',
email: 'invalid-email',
age: 15
});
} catch (error) {
console.error(error.message);
// 输出: Validation failed: {"name":"Name cannot be empty","email":"Invalid email format","age":"Age must be at least 18"}
}
```
### 自定义验证中间件
```typescript
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
export function validationMiddleware<T extends object>(
type: new () => T
) {
return async (req: any, res: any, next: any) => {
const dto = plainToClass(type, req.body);
const errors = await validate(dto);
if (errors.length > 0) {
const formattedErrors = formatValidationErrors(errors);
return res.status(400).json({
error: 'Validation failed',
details: formattedErrors
});
}
req.body = dto;
next();
};
}
// 在 Express 中使用
import express from 'express';
const app = express();
app.post('/users',
validationMiddleware(User),
async (req, res) => {
const user = await userRepository.save(req.body);
res.json(user);
}
);
```
## 验证器装饰器详解
### 字符串验证
```typescript
@Entity()
export class User {
@Column()
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(100)
@IsAlphanumeric()
name: string;
@Column()
@IsEmail()
@IsLowercase()
email: string;
@Column()
@IsUrl()
website?: string;
@Column()
@IsPhoneNumber(null) // 需要安装 class-validator-phone-number
phone?: string;
}
```
### 数字验证
```typescript
@Entity()
export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2 })
@IsNumber()
@Min(0)
@Max(999999.99)
price: number;
@Column({ type: 'int' })
@IsInt()
@IsPositive()
stock: number;
@Column({ type: 'int' })
@IsInt()
@IsDivisibleBy(10)
quantity: number;
}
```
### 日期验证
```typescript
@Entity()
export class Event {
@Column({ type: 'date' })
@IsDateString()
@IsBefore('endDate')
startDate: Date;
@Column({ type: 'date' })
@IsDateString()
@IsAfter('startDate')
endDate: Date;
@Column({ type: 'date' })
@IsDateString()
@IsFuture()
registrationDeadline?: Date;
}
```
### 数组和对象验证
```typescript
@Entity()
export class User {
@Column({ type: 'simple-array' })
@IsArray()
@ArrayNotEmpty()
@ArrayMinSize(1)
@ArrayMaxSize(10)
@IsString({ each: true })
tags: string[];
@Column({ type: 'json', nullable: true })
@IsObject()
@IsNotEmptyObject()
metadata?: Record<string, any>;
@Column({ type: 'simple-array', nullable: true })
@IsArray()
@ArrayUnique()
@IsEmail({ each: true })
additionalEmails?: string[];
}
```
## 验证最佳实践
### 1. 分层验证
```typescript
// 实体层验证:数据库级别的验证
@Entity()
export class User {
@Column()
@IsNotEmpty()
@IsString()
name: string;
@BeforeInsert()
@BeforeUpdate()
async validateEntity() {
const errors = await validate(this);
if (errors.length > 0) {
throw new Error(`Entity validation failed: ${JSON.stringify(errors)}`);
}
}
}
// DTO 层验证:API 请求级别的验证
class CreateUserDto {
@IsNotEmpty()
@IsString()
@MinLength(2)
@MaxLength(100)
name: string;
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@MinLength(8)
password: string;
}
// 在服务层使用 DTO 验证
async function createUser(dto: CreateUserDto) {
const errors = await validate(dto);
if (errors.length > 0) {
throw new ValidationException(errors);
}
const user = new User();
Object.assign(user, dto);
return await userRepository.save(user);
}
```
### 2. 异步验证
```typescript
@ValidatorConstraint({ name: 'isEmailUnique', async: true })
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
async validate(email: string) {
const user = await userRepository.findOne({
where: { email }
});
return !user;
}
defaultMessage() {
return 'Email already exists';
}
}
@Entity()
export class User {
@Column({ unique: true })
@IsEmailUnique()
email: string;
}
```
### 3. 国际化错误消息
```typescript
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint({ name: 'customValidator', async: false })
export class CustomValidatorConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
return true;
}
defaultMessage(args: ValidationArguments) {
// 根据语言环境返回不同的错误消息
const locale = args.object['locale'] || 'en';
const messages = {
en: 'Custom validation failed',
zh: '自定义验证失败',
ja: 'カスタム検証に失敗しました'
};
return messages[locale] || messages.en;
}
}
```
### 4. 性能优化
```typescript
// 避免在验证器中执行耗时操作
@ValidatorConstraint({ name: 'isUnique', async: true })
export class IsUniqueConstraint implements ValidatorConstraintInterface {
private cache = new Map<string, boolean>();
async validate(value: any, args: ValidationArguments) {
const cacheKey = `${args.targetName}.${args.property}.${value}`;
// 检查缓存
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// 执行验证
const result = await this.checkUniqueness(value, args);
// 缓存结果
this.cache.set(cacheKey, result);
return result;
}
private async checkUniqueness(value: any, args: ValidationArguments): Promise<boolean> {
// 实际的唯一性检查逻辑
return true;
}
}
```
### 5. 测试验证器
```typescript
import { validate } from 'class-validator';
describe('User Validation', () => {
it('should validate valid user', async () => {
const user = new User();
user.name = 'John Doe';
user.email = 'john@example.com';
user.age = 25;
const errors = await validate(user);
expect(errors.length).toBe(0);
});
it('should fail validation for invalid email', async () => {
const user = new User();
user.name = 'John Doe';
user.email = 'invalid-email';
user.age = 25;
const errors = await validate(user);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].constraints).toHaveProperty('isEmail');
});
it('should fail validation for underage user', async () => {
const user = new User();
user.name = 'John Doe';
user.email = 'john@example.com';
user.age = 15;
const errors = await validate(user);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].constraints).toHaveProperty('min');
});
});
```
## 验证器与其他库集成
### 与 Joi 集成
```typescript
import * as Joi from 'joi';
const userSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).max(120).optional(),
password: Joi.string().min(8).required()
});
@Entity()
export class User {
@BeforeInsert()
@BeforeUpdate()
async validateWithJoi() {
const { error } = userSchema.validate(this);
if (error) {
throw new Error(`Validation failed: ${error.details[0].message}`);
}
}
}
```
### 与 Zod 集成
```typescript
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).max(120).optional(),
password: z.string().min(8)
});
@Entity()
export class User {
@BeforeInsert()
@BeforeUpdate()
async validateWithZod() {
const result = userSchema.safeParse(this);
if (!result.success) {
throw new Error(`Validation failed: ${JSON.stringify(result.error.errors)}`);
}
}
}
```
TypeORM 的验证器功能提供了强大的数据验证能力,合理使用验证器可以确保数据的完整性和一致性,提高应用程序的健壮性。
服务端 · 2月18日 19:12