← Назад к вопросам

Что такое 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 кода.

Что такое repository в ORM? | PrepBro