Repository pattern w Spring Data

TL;DR: Repository pattern w Spring Data to elegancki sposób oddzielenia logiki biznesowej od warstwy dostępu do danych. Spring Data JPA automatycznie generuje implementacje na podstawie interfejsów, oferując gotowe metody CRUD i możliwość definiowania custom queries przez nazwy metod lub @Query.

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
Wymagania wstępne: Podstawowa znajomość Spring Boot, JPA/Hibernate, adnotacji @Entity. Przydatna znajomość SQL i wzorców projektowych.

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!

Repository pattern – wzorzec projektowy który enkapsuluje logikę potrzebną do dostępu do źródeł danych. Centralizuje często używane funkcje dostępu do danych, zapewniając lepszą możliwość utrzymania kodu.

### 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:

InterfejsMożliwościKiedy używać
RepositoryMarker interface, brak metodGdy chcesz tylko custom metody
CrudRepositoryPodstawowe CRUD operacjeProste aplikacje, podstawowe potrzeby
PagingAndSortingRepositoryCRUD + paginacja + sortowanieGdy potrzebujesz stronicowania
JpaRepositoryWszystko powyżej + JPA specificsNajbardziej 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 Optional findUserById(Long id) {
        return userRepository.findById(id);
    }
    
    public List findAllUsers() {
        return (List) userRepository.findAll();
    }
    
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}
Pro tip: Zawsze używaj constructor injection zamiast @Autowired na polach. Jest to bardziej testowalne i bezpieczne.

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 Page getUsers(
            @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);
    }
}
Paginacja jest kluczowa w aplikacjach produkcyjnych. Bez niej zapytanie o wszystkich użytkowników może zwrócić miliony rekordów i zabić aplikację.

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

Popularne keywords:
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);
}
Uwaga: Metody z @Modifying muszą być używane w kontekście @Transactional, inaczej Spring rzuci wyjątek.

### 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
        Optional found = 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);
    }
}
Pro tip: @DataJpaTest automatycznie konfiguruje in-memory bazę danych (H2) i ładuje tylko komponenty JPA. Testy są szybkie i izolowane.

Best practices i częste błędy

### ✅ Dobre praktyki

1. Używaj Optional – metody findBy* zwracają Optional zamiast null
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

Typowy błąd: Zwracanie List zamiast Page dla dużych zbiorów danych. To może spowodować OutOfMemoryError w produkcji.
// ❌ ŹLE - może zwrócić miliony rekordów
List findByStatus(UserStatus status);

// ✅ DOBRZE - z paginacją
Page findByStatus(UserStatus status, Pageable pageable);
Pułapka: Używanie getOne() zamiast findById(). Metoda getOne() zwraca lazy proxy i może rzucić LazyInitializationException poza kontekstem transakcji.
Kiedy używać @Query zamiast method naming?

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.

Czy mogę mieć wiele Repository dla jednej encji?

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).

Jak testować custom @Query?

Używaj @DataJpaTest z TestEntityManager. Dla natywnych zapytań SQL testuj na takiej samej bazie jak produkcja (nie H2), używając Testcontainers.

Czy Repository pattern wpływa na wydajność?

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ć.

Jak dodać custom logic do Repository?

Stwórz dodatkowy interfejs (np. UserRepositoryCustom) z custom metodami, zaimplementuj go w klasie (UserRepositoryImpl), a główne Repository niech dziedziczy oba interfejsy.

Dlaczego findAll() zwraca Iterable a nie List?

CrudRepository zwraca Iterable dla elastyczności – różne implementacje mogą zwracać różne typy kolekcji. JpaRepository już zwraca List i Page.

Jak obsłużyć sortowanie z wieloma kryteriami?

Użyj Sort.by() z wieloma kryteriami: Sort.by(„lastName”).and(Sort.by(„firstName”).descending()) lub Orders w Pageable.

Przydatne zasoby:

🚀 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!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry