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

Для чего нужен репозиторий?

1.7 Middle🔥 251 комментариев
#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Repository Паттерн: Назначение и Применение

Определение

Repository — это паттерн проектирования, который создаёт абстракцию между бизнес-логикой приложения и слоем персистентности (доступом к данным), скрывая детали взаимодействия с БД.

Проблема Без Repository

❌ Неправильно: Прямой доступ к БД

@Service
public class UserService {
    
    @Autowired
    private DataSource dataSource;
    
    public User getUserById(Long id) {
        // ПЛОХО: SQL код прямо в сервис
        try (Connection conn = dataSource.getConnection()) {
            String sql = "SELECT id, name, email FROM users WHERE id = ?";
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setLong(1, id);
            ResultSet rs = stmt.executeQuery();
            
            if (rs.next()) {
                return new User(rs.getLong(1), rs.getString(2), rs.getString(3));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
    
    public void saveUser(User user) {
        // ПЛОХО: Смешивание бизнес-логики с SQL
        try (Connection conn = dataSource.getConnection()) {
            String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setString(1, user.getName());
            stmt.setString(2, user.getEmail());
            stmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    public List<User> searchByEmail(String email) {
        // ПЛОХО: Ещё больше SQL кода
        // ...
    }
}

// Проблемы:
// - UserService смешивает бизнес-логику и доступ к БД
// - Тяжело тестировать (нужна реальная БД)
// - Сложно менять реализацию (например, с MySQL на PostgreSQL)
// - SQL код дублируется в разных местах
// - Нарушен принцип Single Responsibility

Решение: Repository Паттерн

✅ Правильно: Repository абстракция

// 1. Интерфейс Repository (контракт)
public interface UserRepository {
    
    User findById(Long id);
    
    void save(User user);
    
    void update(User user);
    
    void delete(Long id);
    
    List<User> findAll();
    
    List<User> findByEmail(String email);
}

// 2. Реализация Repository (детали БД)
@Repository
public class UserRepositoryImpl implements UserRepository {
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    public User findById(Long id) {
        // SQL код ТОЛЬКО здесь
        try (Connection conn = dataSource.getConnection()) {
            String sql = "SELECT id, name, email FROM users WHERE id = ?";
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setLong(1, id);
            ResultSet rs = stmt.executeQuery();
            
            if (rs.next()) {
                return new User(rs.getLong(1), rs.getString(2), rs.getString(3));
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to find user", e);
        }
        return null;
    }
    
    @Override
    public void save(User user) {
        try (Connection conn = dataSource.getConnection()) {
            String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setString(1, user.getName());
            stmt.setString(2, user.getEmail());
            stmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("Failed to save user", e);
        }
    }
    
    @Override
    public List<User> findByEmail(String email) {
        List<User> users = new ArrayList<>();
        try (Connection conn = dataSource.getConnection()) {
            String sql = "SELECT id, name, email FROM users WHERE email = ?";
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setString(1, email);
            ResultSet rs = stmt.executeQuery();
            
            while (rs.next()) {
                users.add(new User(rs.getLong(1), rs.getString(2), rs.getString(3)));
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to find users by email", e);
        }
        return users;
    }
}

// 3. Сервис использует Repository (чистая бизнес-логика)
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public User getUserById(Long id) {
        // ХОРОШО: Бизнес-логика без SQL
        return userRepository.findById(id);
    }
    
    public void registerUser(String name, String email) {
        // ХОРОШО: Валидация и бизнес-логика
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        
        List<User> existing = userRepository.findByEmail(email);
        if (!existing.isEmpty()) {
            throw new UserAlreadyExistsException("User with email already exists");
        }
        
        User user = new User(name, email);
        userRepository.save(user);
    }
    
    public void updateUserEmail(Long userId, String newEmail) {
        User user = userRepository.findById(userId);
        if (user == null) {
            throw new UserNotFoundException("User not found");
        }
        
        // Валидация
        if (!isValidEmail(newEmail)) {
            throw new InvalidEmailException("Invalid email format");
        }
        
        user.setEmail(newEmail);
        userRepository.update(user);
    }
    
    private boolean isValidEmail(String email) {
        return email.contains("@") && email.contains(".");
    }
}

// 4. Контроллер использует Сервис
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(new UserDTO(user));
    }
    
    @PostMapping
    public ResponseEntity<UserDTO> registerUser(@RequestBody RegisterRequest request) {
        try {
            userService.registerUser(request.getName(), request.getEmail());
            return ResponseEntity.status(HttpStatus.CREATED).build();
        } catch (UserAlreadyExistsException e) {
            return ResponseEntity.badRequest().build();
        }
    }
}

Spring Data JPA Repository (Упрощение)

JPA Repository (БЕЗ реализации)

// Spring Data автоматически реализует Repository!
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // CRUD операции встроены:
    // findById(), save(), delete(), findAll()
    
    // Кастомные методы (Spring Data сам создаст SQL)
    List<User> findByEmail(String email);
    
    List<User> findByNameContaining(String name);
    
    Optional<User> findByEmailAndName(String email, String name);
}

// Сервис использует JPA Repository
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public User getUserById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
    
    public List<User> searchByEmail(String email) {
        return userRepository.findByEmail(email);
    }
}

Преимущества Repository Паттерна

1. Разделение Слоёв (Separation of Concerns)

┌─────────────────────┐
│   Presentation      │ (Контроллеры, Views)
├─────────────────────┤
│   Business Logic    │ (Сервисы) — использует Repository
├─────────────────────┤
│   Data Access       │ (Repository) — скрывает как получаются данные
├─────────────────────┤
│   Persistence       │ (БД)
└─────────────────────┘

Каждый слой отвечает за свою задачу

2. Тестируемость

@RunWith(SpringRunner.class)
public class UserServiceTest {
    
    @InjectMocks
    private UserService userService;
    
    @Mock
    private UserRepository userRepository;  // Mock вместо реальной БД
    
    @Test
    public void testGetUserById() {
        // Arrange
        User mockUser = new User(1L, "John", "john@mail.com");
        when(userRepository.findById(1L)).thenReturn(mockUser);
        
        // Act
        User result = userService.getUserById(1L);
        
        // Assert
        assertEquals("John", result.getName());
        
        // Тест БЕЗ реальной БД!
    }
    
    @Test
    public void testRegisterUserDuplicate() {
        // Arrange
        List<User> existingUsers = new ArrayList<>();
        existingUsers.add(new User(1L, "John", "john@mail.com"));
        when(userRepository.findByEmail("john@mail.com")).thenReturn(existingUsers);
        
        // Act & Assert
        assertThrows(UserAlreadyExistsException.class, 
            () -> userService.registerUser("John Doe", "john@mail.com"));
    }
}

3. Простота Изменения Реализации

// Если менять БД с MySQL на MongoDB:

// Было (JDBC)
@Repository
public class UserRepositoryJdbc implements UserRepository { }

// Стало (MongoDB)
@Repository
public class UserRepositoryMongo implements UserRepository {
    
    @Autowired
    private MongoTemplate mongoTemplate;
    
    @Override
    public User findById(Long id) {
        return mongoTemplate.findById(id, User.class);
    }
}

// Сервис НЕ меняется! Используется один интерфейс
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;  // Работает с любой реализацией
}

4. Переиспользование Кода

// Один Repository используется всеми сервисами
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

@Service
public class AuthService {
    @Autowired
    private UserRepository userRepository;  // Переиспользуем
}

@Service
public class NotificationService {
    @Autowired
    private UserRepository userRepository;  // Переиспользуем
}

5. Аудит и Логирование

@Repository
public class UserRepositoryAudited implements UserRepository {
    
    @Autowired
    private UserRepository delegate;
    
    @Autowired
    private AuditLogger auditLogger;
    
    @Override
    public User findById(Long id) {
        auditLogger.log("SELECT", "users", "id=" + id);
        return delegate.findById(id);
    }
    
    @Override
    public void save(User user) {
        auditLogger.log("INSERT", "users", user.toString());
        delegate.save(user);
    }
}

Repository vs DAO

Repository:
- Работает с агрегатами (Domain Driven Design)
- Коллекционная семантика
- Высокоуровневая абстракция
- Предпочитаемый в современных приложениях

DAO (Data Access Object):
- Работает с данными (CRUD)
- Низкоуровневая абстракция
- Более старый паттерн
- Всё ещё используется, но Repository предпочтительнее

Best Practices

public class RepositoryBestPractices {
    
    // ✅ Repository использует БИЗНЕС язык
    // ❌ Не repository.findByColumnName("value");
    // ✅ repository.findActiveUsersInCity("New York");
    
    // ✅ Repository возвращает доменные объекты (сущности)
    // ❌ Не repository.findUserDTO();
    // ✅ User user = repository.findById(1L);
    
    // ✅ Repository содержит методы ПОИСКА и СОХРАНЕНИЯ
    // ❌ Не repository.sendEmail();
    // ❌ Не repository.performComplexCalculation();
    
    // ✅ Используй Optional для методов, которые могут вернуть null
    Optional<User> findById(Long id);
    
    // ✅ Используй Collections для множественных результатов
    List<User> findAll();
    
    // ✅ Кешируй результаты если часто используются
    @Cacheable("users")
    List<User> findAll();
}

Repository с Pagination и Sorting

@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
    
    // Spring Data автоматически добавит поддержку Page и Sort
    Page<User> findAll(Pageable pageable);
    
    Page<User> findByNameContaining(String name, Pageable pageable);
}

// Использование
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public Page<User> getUsers(int page, int size, String sortBy) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        return userRepository.findAll(pageable);
    }
    
    public Page<User> searchUsers(String query, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findByNameContaining(query, pageable);
    }
}

Заключение

Repository паттерн нужен для:

  • Абстракции слоя персистентности (отделение БД от бизнес-логики)
  • Упрощения тестирования (легко создавать mock'и)
  • Переиспользования кода (один Repository для многих сервисов)
  • Гибкости реализации (легко менять БД)
  • Читаемости кода (сервисы содержат только бизнес-логику)

Правило: Всегда используй Repository между бизнес-логикой и слоем БД. Это проверенный паттерн, который делает код чище и тестируемее.