Dlaczego Repository pattern jest ważny?
W tradycyjnym podejściu kod dostępu do bazy danych był rozrzucony po całej aplikacji. **Repository pattern** rozwiązuje ten problem, tworząc warstwę abstrakcji między logiką biznesową a bazą danych. To jak posiadanie dedykowanego bibliotekarza, który wie gdzie znaleźć każdą książkę w bibliotece.
Dzięki temu wzorcowi możesz łatwo zmieniać sposób przechowywania danych (z MySQL na PostgreSQL), testować logikę biznesową bez bazy danych, a kod staje się bardziej czytelny i łatwiejszy w utrzymaniu.
Co się nauczysz:
- Jak implementować Repository pattern w Spring Data JPA
- Różnice między CrudRepository, JpaRepository i PagingAndSortingRepository
- Tworzenie custom queries przez nazwy metod
- Używanie @Query dla zaawansowanych zapytań
- Best practices i częste pułapki w Repository pattern
Podstawy Repository pattern w Spring Data
Spring Data JPA oferuje gotowe interfejsy, które automatycznie generują implementacje bazowe operacji CRUD. Nie musisz pisać ani linijki kodu SQL!
### Przykład encji User
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; @Column private Integer age; @Enumerated(EnumType.STRING) private UserStatus status; @CreationTimestamp private LocalDateTime createdAt; // konstruktory, gettery, settery public User() {} public User(String email, String firstName, String lastName) { this.email = email; this.firstName = firstName; this.lastName = lastName; this.status = UserStatus.ACTIVE; } // gettery i settery... }
Typy Repository w Spring Data
Spring Data oferuje kilka poziomów interfejsów Repository:
Interfejs | Możliwości | Kiedy używać |
---|---|---|
Repository | Marker interface, brak metod | Gdy chcesz tylko custom metody |
CrudRepository | Podstawowe CRUD operacje | Proste aplikacje, podstawowe potrzeby |
PagingAndSortingRepository | CRUD + paginacja + sortowanie | Gdy potrzebujesz stronicowania |
JpaRepository | Wszystko powyżej + JPA specifics | Najbardziej uniwersalny wybór |
### Podstawowe Repository – CrudRepository
@Repository public interface UserRepository extends CrudRepository{ // Spring Data automatycznie generuje implementacje: // save(User user) // findById(Long id) // findAll() // deleteById(Long id) // count() // existsById(Long id) }
### Użycie w Service
@Service @Transactional public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User createUser(String email, String firstName, String lastName) { User user = new User(email, firstName, lastName); return userRepository.save(user); } public OptionalfindUserById(Long id) { return userRepository.findById(id); } public List findAllUsers() { return (List ) userRepository.findAll(); } public void deleteUser(Long id) { userRepository.deleteById(id); } }
JpaRepository – najlepszy wybór dla większości projektów
@Repository public interface UserRepository extends JpaRepository{ // Dziedziczy wszystkie metody z CrudRepository // Plus dodatkowe JPA-specific metody: // saveAndFlush(User user) // deleteInBatch(Iterable users) // getOne(Long id) - lazy loading // findAll(Sort sort) // findAll(Pageable pageable) }
### Paginacja i sortowanie
@RestController @RequestMapping("/api/users") public class UserController { private final UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping public PagegetUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "lastName") String sortBy) { Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy)); return userRepository.findAll(pageable); } }
Custom queries – metody derive z nazw
Spring Data potrafi automatycznie generować zapytania na podstawie nazw metod:
@Repository public interface UserRepository extends JpaRepository{ // SELECT * FROM users WHERE email = ? Optional findByEmail(String email); // SELECT * FROM users WHERE first_name = ? AND last_name = ? List findByFirstNameAndLastName(String firstName, String lastName); // SELECT * FROM users WHERE age > ? List findByAgeGreaterThan(Integer age); // SELECT * FROM users WHERE last_name LIKE ? List findByLastNameContaining(String lastName); // SELECT * FROM users WHERE status = ? ORDER BY created_at DESC List findByStatusOrderByCreatedAtDesc(UserStatus status); // SELECT COUNT(*) FROM users WHERE age BETWEEN ? AND ? long countByAgeBetween(Integer minAge, Integer maxAge); // SELECT * FROM users WHERE email = ? AND status = ? boolean existsByEmailAndStatus(String email, UserStatus status); }
### Słowa kluczowe w nazwach metod
– And, Or – łączenie warunków
– Between, LessThan, GreaterThan – porównania
– Like, Containing, StartingWith, EndingWith – pattern matching
– OrderBy, Top, First – sortowanie i limitowanie
– Distinct, IgnoreCase – modyfikatory
@Query – gdy nazwy metod nie wystarczają
Dla bardziej skomplikowanych zapytań możesz użyć adnotacji @Query:
@Repository public interface UserRepository extends JpaRepository{ // JPQL query @Query("SELECT u FROM User u WHERE u.age > :age AND u.status = :status") List findActiveUsersAboveAge(@Param("age") Integer age, @Param("status") UserStatus status); // Native SQL query @Query(value = "SELECT * FROM users WHERE YEAR(created_at) = :year", nativeQuery = true) List findUsersCreatedInYear(@Param("year") int year); // Modifying query for updates @Modifying @Query("UPDATE User u SET u.status = :status WHERE u.age < :age") int updateStatusForUsersBelow(@Param("age") Integer age, @Param("status") UserStatus status); // Projection - tylko wybrane pola @Query("SELECT u.firstName, u.lastName, u.email FROM User u WHERE u.status = :status") List findUserProjectionsByStatus(@Param("status") UserStatus status); }
### Interface projection
public interface UserProjection { String getFirstName(); String getLastName(); String getEmail(); // Computed property default String getFullName() { return getFirstName() + " " + getLastName(); } }
Testowanie Repository
@DataJpaTest public class UserRepositoryTest { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test public void whenFindByEmail_thenReturnUser() { // Given User user = new User("john@example.com", "John", "Doe"); entityManager.persist(user); entityManager.flush(); // When Optionalfound = userRepository.findByEmail("john@example.com"); // Then assertThat(found).isPresent(); assertThat(found.get().getFirstName()).isEqualTo("John"); } @Test public void whenFindByAgeGreaterThan_thenReturnCorrectUsers() { // Given User young = new User("young@example.com", "Young", "User"); young.setAge(20); User old = new User("old@example.com", "Old", "User"); old.setAge(40); entityManager.persist(young); entityManager.persist(old); entityManager.flush(); // When List users = userRepository.findByAgeGreaterThan(30); // Then assertThat(users).hasSize(1); assertThat(users.get(0).getAge()).isEqualTo(40); } }
Best practices i częste błędy
### ✅ Dobre praktyki
2. Nie zwracaj Entity z Controller – używaj DTO lub projection
3. Transakcje w Service – nie w Repository
4. Paginacja zawsze – dla listowania danych
5. Naming conventions – metody opisujące biznes, nie technikę
### ❌ Częste błędy
// ❌ ŹLE - może zwrócić miliony rekordów ListfindByStatus(UserStatus status); // ✅ DOBRZE - z paginacją Page findByStatus(UserStatus status, Pageable pageable);
Użyj @Query gdy nazwa metody stałaby się bardzo długa, potrzebujesz JOIN z innymi tabelami, używasz funkcji agregujących (COUNT, SUM) lub potrzebujesz natywnych zapytań SQL dla optymalizacji wydajności.
Tak, ale to rzadko dobry pomysł. Lepiej mieć jedno główne Repository i opcjonalnie dedykowane Repository dla specjalnych przypadków (np. ReadOnlyUserRepository dla reportów).
Używaj @DataJpaTest z TestEntityManager. Dla natywnych zapytań SQL testuj na takiej samej bazie jak produkcja (nie H2), używając Testcontainers.
Minimalnie. Spring Data generuje implementacje w czasie startu aplikacji. Większy wpływ mają N+1 queries – używaj @EntityGraph lub JOIN FETCH w @Query aby je unikać.
Stwórz dodatkowy interfejs (np. UserRepositoryCustom) z custom metodami, zaimplementuj go w klasie (UserRepositoryImpl), a główne Repository niech dziedziczy oba interfejsy.
CrudRepository zwraca Iterable dla elastyczności – różne implementacje mogą zwracać różne typy kolekcji. JpaRepository już zwraca List
Użyj Sort.by() z wieloma kryteriami: Sort.by(„lastName”).and(Sort.by(„firstName”).descending()) lub Orders w Pageable.
Przydatne zasoby:
- Spring Data JPA Reference
- Spring Guide – Accessing Data with JPA
- Query Methods Documentation
- Spring Data Examples Repository
🚀 Zadanie dla Ciebie
Stwórz kompletny system zarządzania produktami: encję Product (name, price, category, createdAt), Repository z metodami findByCategoryAndPriceGreaterThan, custom @Query dla najpopularniejszych produktów, oraz testy jednostkowe. Dodaj REST endpoint z paginacją i sortowaniem.
Jakiej części Repository pattern używasz najczęściej w swoich projektach? Podziel się swoimi doświadczeniami z custom queries i wydajnością w komentarzach!