Валидация, завязанная на состояние базы данных, отличается от статической проверки тем, что требует асинхронного выполнения. Это означает, что правила проверки не ограничиваются синтаксисом или форматом данных, а опираются на внешние источники: наличие записей, уникальность значений, целостность связей между сущностями.
В рамках class-validator асинхронная валидация реализуется через пользовательские ограничения (custom constraints), поддерживающие работу с промисами и внешними сервисами, включая базы данных через ORM или прямые репозитории.
Для выполнения проверок, зависящих от базы данных, используется
декоратор @ValidatorConstraint с включённым режимом
асинхронности.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} fr om 'class-validator';
@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
async validate(email: string, args: ValidationArguments): Promise<boolean> {
return true;
}
defaultMessage(args: ValidationArguments): string {
return 'Email уже существует';
}
}
Ключевой момент заключается в том, что метод validate
может возвращать Promise<boolean>, что позволяет
выполнять запросы к базе данных без блокировки потока выполнения.
На практике ограничение связывается с сервисом или репозиторием, который выполняет запрос к таблице пользователей.
import { DataSource } from 'typeorm';
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(private dataSource: DataSource) {}
async validate(email: string): Promise<boolean> {
const user = await this.dataSource
.getRepository('User')
.findOne({ wh ere: { email } });
return !user;
}
defaultMessage(): string {
return 'Пользователь с таким email уже существует';
}
}
В данном случае проверка опирается на результат SQL-запроса. Если запись найдена, валидация считается неуспешной.
Для удобства используется обёртка-декоратор, инкапсулирующая логику constraint.
import { registerDecorator, ValidationOptions } fr om 'class-validator';
export function IsEmailUnique(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
validator: IsEmailUniqueConstraint,
});
};
}
Использование в DTO:
export class CreateUserDto {
@IsEmailUnique({ message: 'Email занят' })
email: string;
}
Часто требуется убедиться, что внешний идентификатор действительно существует в базе данных. Это типичный сценарий для foreign key на уровне бизнес-логики.
@ValidatorConstraint({ async: true })
export class ExistsInDatabaseConstraint implements ValidatorConstraintInterface {
constructor(private dataSource: DataSource) {}
async validate(id: number): Promise<boolean> {
const record = await this.dataSource
.getRepository('Category')
.findOne({ wh ere: { id } });
return Boolean(record);
}
defaultMessage(): string {
return 'Сущность не существует';
}
}
Такой подход используется при проверке categoryId,
roleId, departmentId и аналогичных связей.
Валидация может зависеть не только от одного поля, но и от состояния
всего объекта DTO. В ValidationArguments доступен объект
object, позволяющий учитывать дополнительные параметры.
async validate(value: number, args: ValidationArguments): Promise<boolean> {
const dto = args.object as any;
if (dto.isActive === false) {
return true;
}
const exists = await this.repo.findOneBy({ id: value });
return !!exists;
}
Это позволяет реализовывать условную валидацию, зависящую от других полей.
При использовании Prisma доступ к базе осуществляется через PrismaClient.
import { PrismaClient } fr om '@prisma/client';
const prisma = new PrismaClient();
@ValidatorConstraint({ async: true })
export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface {
async validate(username: string): Promise<boolean> {
const user = await prisma.user.findUnique({
wh ere: { username },
});
return user === null;
}
defaultMessage(): string {
return 'Username уже используется';
}
}
Prisma обеспечивает более строгую типизацию, что снижает вероятность ошибок в запросах внутри валидаторов.
В реальных приложениях constraint не должен напрямую создавать подключения к базе данных. Используется внедрение зависимостей через контейнер.
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(private userService: UserService) {}
async validate(email: string): Promise<boolean> {
return !(await this.userService.findByEmail(email));
}
}
Сервис инкапсулирует доступ к базе, что позволяет переиспользовать бизнес-логику и снижает связность.
При большом количестве проверок, обращающихся к базе данных, возникает риск деградации производительности.
Основные источники нагрузки:
Для минимизации нагрузки применяется агрегация запросов.
const ids = values.map(v => v.id);
const existing = await repo.findBy({ id: In(ids) });
const existingSet = new Set(existing.map(e => e.id));
Такой подход заменяет множество запросов одним.
При повторяющихся валидациях одного и того же значения используется кэширование.
const cache = new Map<string, boolean>();
async validate(email: string): Promise<boolean> {
if (cache.has(email)) {
return cache.get(email)!;
}
const user = await repo.findOneBy({ email });
const result = !user;
cache.set(email, result);
return result;
}
Кэш особенно эффективен при массовой валидации списков или повторных запросах одного пользователя.
Валидация на уровне DTO не должна превращаться в полноценный слой бизнес-логики. При избыточном количестве проверок ухудшается читаемость и увеличивается время обработки запроса.
Часто применяется стратегия переноса части проверок в сервисный слой, оставляя в class-validator только критические ограничения:
Несколько constraint-ов могут выполняться последовательно или параллельно в зависимости от конфигурации. Каждый из них может выполнять независимый запрос к базе.
@Validate(IsEmailUnique)
@Validate(IsEmailAllowedDomain)
email: string;
Каждый constraint формирует отдельный этап проверки, что требует контроля общего количества запросов и их стоимости.
При обращении к базе данных внутри валидаторов возможны исключения: потеря соединения, таймауты, ошибки запросов.
Рекомендуется изолировать такие ситуации и интерпретировать их как неуспешную валидацию.
async validate(id: number): Promise<boolean> {
try {
const entity = await repo.findOneBy({ id });
return !!entity;
} catch (e) {
return false;
}
}
Это предотвращает проброс ошибок уровня инфраструктуры в слой валидации DTO.
Асинхронные проверки с доступом к базе данных выполняются в рамках одного жизненного цикла запроса. Это означает, что каждая валидация увеличивает общее время ответа.
При проектировании схемы DTO учитывается баланс между:
Оптимизация достигается за счёт минимизации количества уникальных запросов и переиспользования результатов внутри одного запроса.