← Назад к вопросам
Что такое repository в ORM?
2.0 Middle🔥 172 комментариев
#Архитектура и паттерны#Базы данных и SQL
Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Repository паттерн в ORM: инкапсуляция доступа к данным
Что такое Repository?
Repository — это паттерн проектирования, который инкапсулирует логику доступа к данным. Вместо того чтобы писать SQL или ORM запросы по всему коду, мы создаем специальный класс (Repository), который отвечает за все операции с конкретной сущностью.
Основная идея:
- Один Repository отвечает за одну сущность (User, Post, Comment)
- Все CRUD операции сосредоточены в одном месте
- Бизнес-логика отделена от доступа к БД
- Легко менять реализацию (с SQL на NoSQL)
Архитектура: Layered Architecture
Presentation Layer (Controllers)
↓
Application Layer (Services)
↓
Domain Layer (Business Logic)
↓
Infrastructure Layer (Repositories)
↓
Database
Правило: зависимости идут только вниз (сверху вниз), не снизу вверх.
Пример: Repository для User
Интерфейс (контракт):
interface IUserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
findAll(filters?: UserFilters): Promise<User[]>;
create(data: CreateUserDto): Promise<User>;
update(id: string, data: UpdateUserDto): Promise<User>;
delete(id: string): Promise<boolean>;
count(filters?: UserFilters): Promise<number>;
}
Реализация (TypeORM):
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(User)
private readonly repository: Repository<User>,
) {}
async findById(id: string): Promise<User | null> {
return this.repository.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.repository.findOne({ where: { email } });
}
async findAll(filters?: UserFilters): Promise<User[]> {
let query = this.repository.createQueryBuilder('user');
if (filters?.role) {
query = query.where('user.role = :role', { role: filters.role });
}
if (filters?.isActive) {
query = query.andWhere('user.isActive = :isActive', {
isActive: filters.isActive,
});
}
return query.orderBy('user.createdAt', 'DESC').getMany();
}
async create(data: CreateUserDto): Promise<User> {
const user = this.repository.create(data);
return this.repository.save(user);
}
async update(id: string, data: UpdateUserDto): Promise<User> {
await this.repository.update(id, data);
return this.findById(id);
}
async delete(id: string): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected > 0;
}
async count(filters?: UserFilters): Promise<number> {
let query = this.repository.createQueryBuilder('user');
if (filters?.role) {
query = query.where('user.role = :role', { role: filters.role });
}
return query.getCount();
}
}
Service использует Repository
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly emailService: EmailService,
) {}
async registerUser(dto: RegisterUserDto): Promise<UserResponseDto> {
// Проверяем, что email не существует
const existingUser = await this.userRepository.findByEmail(dto.email);
if (existingUser) {
throw new ConflictException('Email already registered');
}
// Создаем пользователя
const user = await this.userRepository.create({
email: dto.email,
name: dto.name,
passwordHash: await hashPassword(dto.password),
});
// Отправляем приветственное письмо
await this.emailService.sendWelcomeEmail(user.email);
return new UserResponseDto(user);
}
async getUsersByRole(role: 'admin' | 'user'): Promise<UserResponseDto[]> {
const users = await this.userRepository.findAll({ role });
return users.map(user => new UserResponseDto(user));
}
async updateUserProfile(
userId: string,
dto: UpdateProfileDto,
): Promise<UserResponseDto> {
const user = await this.userRepository.update(userId, {
name: dto.name,
bio: dto.bio,
});
return new UserResponseDto(user);
}
}
Controller использует Service
@Controller('api/v1/users')
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('register')
async register(@Body() dto: RegisterUserDto) {
return this.userService.registerUser(dto);
}
@Get()
async getUsers(@Query() query: GetUsersQueryDto) {
return this.userService.getUsersByRole(query.role);
}
@Patch(':id')
async updateProfile(
@Param('id') id: string,
@Body() dto: UpdateProfileDto,
) {
return this.userService.updateUserProfile(id, dto);
}
}
Преимущества Repository паттерна
1. Разделение ответственности
Controller: только HTTP логика
Service: только бизнес-логика
Repository: только доступ к БД
2. Тестируемость
// Mock Repository для тестирования Service
class MockUserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
return { id: '1', email: 'test@example.com', name: 'Test' };
}
// ... остальные методы
}
// Тест Service без взаимодействия с БД
test('registerUser успешно регистрирует пользователя', async () => {
const service = new UserService(
new MockUserRepository(),
mockEmailService,
);
const result = await service.registerUser({
email: 'new@example.com',
name: 'New User',
password: '123456',
});
expect(result.email).toBe('new@example.com');
});
3. Переиспользование кода
Один Repository используется несколькими Service:
@Injectable()
export class AdminService {
constructor(private readonly userRepository: UserRepository) {}
async getAllUsers(): Promise<User[]> {
return this.userRepository.findAll();
}
}
@Injectable()
export class AuthService {
constructor(private readonly userRepository: UserRepository) {}
async validateCredentials(email: string, password: string): Promise<User> {
const user = await this.userRepository.findByEmail(email);
// ...
}
}
4. Легко менять реализацию
// Была PostgreSQL реализация
export class PostgresUserRepository implements IUserRepository { /* ... */ }
// Переходим на MongoDB
export class MongoUserRepository implements IUserRepository {
constructor(private readonly mongoDb: Database) {}
async findById(id: string): Promise<User | null> {
return this.mongoDb.collection('users').findOne({ _id: new ObjectId(id) });
}
// ... остальные методы
}
// Service не меняется! Через dependency injection подменяем реализацию
Repository с Query Builder (сложные запросы)
@Injectable()
export class PostRepository {
constructor(
@InjectRepository(Post)
private readonly repository: Repository<Post>,
) {}
// Сложный запрос с JOIN и фильтрацией
async findUserPostsWithComments(
userId: string,
limit: number = 10,
offset: number = 0,
): Promise<Post[]> {
return this.repository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'user')
.leftJoinAndSelect('post.comments', 'comment')
.leftJoinAndSelect('comment.author', 'commentAuthor')
.where('post.authorId = :userId', { userId })
.andWhere('post.publishedAt IS NOT NULL')
.orderBy('post.createdAt', 'DESC')
.addOrderBy('comment.createdAt', 'ASC')
.skip(offset)
.take(limit)
.getMany();
}
// Агрегация
async getPostStatistics(userId: string): Promise<PostStats> {
const result = await this.repository
.createQueryBuilder('post')
.select('COUNT(post.id)', 'totalPosts')
.addSelect('AVG(post.views)', 'avgViews')
.addSelect('SUM(post.likes)', 'totalLikes')
.where('post.authorId = :userId', { userId })
.getRawOne();
return result;
}
}
Repository Pattern в Mongoose (MongoDB)
@Injectable()
export class UserRepository {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
async findById(id: string): Promise<User | null> {
return this.userModel.findById(id).exec();
}
async findByEmail(email: string): Promise<User | null> {
return this.userModel.findOne({ email }).exec();
}
async findAll(filters?: UserFilters): Promise<User[]> {
let query = this.userModel.find();
if (filters?.role) {
query = query.where('role', filters.role);
}
if (filters?.isActive) {
query = query.where('isActive', filters.isActive);
}
return query.sort({ createdAt: -1 }).exec();
}
async create(data: CreateUserDto): Promise<User> {
const user = new this.userModel(data);
return user.save();
}
async update(id: string, data: UpdateUserDto): Promise<User> {
return this.userModel
.findByIdAndUpdate(id, data, { new: true })
.exec();
}
async delete(id: string): Promise<boolean> {
const result = await this.userModel.deleteOne({ _id: id }).exec();
return result.deletedCount > 0;
}
}
База Repository (Generic Repository)
Для избежания дублирования CRUD методов:
@Injectable()
export abstract class BaseRepository<Entity extends { id: string }> {
constructor(
@InjectRepository(Entity)
protected repository: Repository<Entity>,
) {}
async findById(id: string): Promise<Entity | null> {
return this.repository.findOne({ where: { id } } as any);
}
async findAll(): Promise<Entity[]> {
return this.repository.find();
}
async create(data: Partial<Entity>): Promise<Entity> {
const entity = this.repository.create(data);
return this.repository.save(entity);
}
async update(id: string, data: Partial<Entity>): Promise<Entity> {
await this.repository.update(id, data);
return this.findById(id);
}
async delete(id: string): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected > 0;
}
}
// Наследование BaseRepository
@Injectable()
export class UserRepository extends BaseRepository<User> {
constructor(
@InjectRepository(User)
repository: Repository<User>,
) {
super(repository);
}
// Специфичные для User методы
async findByEmail(email: string): Promise<User | null> {
return this.repository.findOne({ where: { email } });
}
}
Практический пример из production
// Весь flow: Controller -> Service -> Repository -> DB
@Post(':id/activate')
async activateUser(@Param('id') id: string) {
// Controller: валидирует параметры
const user = await this.userService.activateUser(id);
return new UserResponseDto(user);
}
// Service: бизнес-логика
async activateUser(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) throw new NotFoundException('User not found');
if (user.isActive) throw new BadRequestException('Already active');
const updatedUser = await this.userRepository.update(id, {
isActive: true,
activatedAt: new Date(),
});
await this.emailService.sendActivationConfirm(user.email);
return updatedUser;
}
// Repository: доступ к БД
async update(id: string, data: Partial<User>): Promise<User> {
await this.repository.update(id, data);
return this.findById(id);
}
Выводы
Repository паттерн — это:
- Инкапсуляция доступа к БД
- Разделение ответственности
- Улучшение тестируемости
- Упрощение изменений архитектуры
- Best practice для современного бэкенда
Это стандартный паттерн в NestJS приложениях и обязателен для production кода.