乐闻世界logo
搜索文章和话题

TypeORM 如何使用验证器?包括 class-validator 的集成和自定义验证器的实现

2月18日 19:12

数据验证是应用程序开发中的重要环节,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 的验证器功能提供了强大的数据验证能力,合理使用验证器可以确保数据的完整性和一致性,提高应用程序的健壮性。

标签:TypeORMClass Validator