1. Wybór bazowego interfejsu repozytorium
Jeśli dopiero zaczynasz, wybór między CrudRepository, PagingAndSortingRepository a JpaRepository łatwo wygląda jak wybór między trzema identycznymi jogurtami, gdzie różni się tylko czcionka na opakowaniu. Kod będzie przecież działał w wielu przypadkach. Ale jest tu haczyk: interfejs repozytorium to twój publiczny kontrakt dostępu do danych, i jeśli wybierzesz go przypadkowo, projekt szybko stanie się „nierówny" i niewygodny do czytania.
Wyobraź sobie, że repozytorium to pilot do twojego magazynu danych. Jeden pilot ma tylko przyciski „wł./wył.", drugi dołożył „głośniej/ciszej", a trzeci — jeszcze normalny wybór źródła, tryby, timer. Można żyć z prostym pilotem, ale jeśli masz w jednym pokoju trzy różne piloty i ciągle zapominasz, który od czego — wieczór przestaje być przyjemny.
W Spring Data historia jest podobna: różne bazowe interfejsy dają różny zakres API, i ważne jest wybranie tego, który stanie się jednolitym baseline'em w twoim projekcie.
2. Mapa Spring Data
W tym temacie przydaje się zrobić mały krok wstecz i zobaczyć, że Spring Data to nie tylko JPA. Spring Data to rodzina projektów, które umieją budować repozytoria dla różnych magazynów: SQL (przez JPA), MongoDB, Redis i innych. Dlatego Spring Data ma „wspólną część", która opisuje abstrakcyjne repozytorium, oraz konkretne rozszerzenia pod konkretne technologie, np. pod JPA.
Bazowe interfejsy można więc postrzegać jak warstwy. Na dole — minimalny kontrakt CRUD, wyżej — umiejętność sortowania i oddawania danych porcjami, jeszcze wyżej — wygody zorientowane na JPA. Nawet jeśli w bibliotekach są dodatkowe pośrednie interfejsy i niuanse wersji, dla inżyniera-praktyka ważniejsza jest myśl: im „bogatszy" bazowy interfejs, tym więcej stabilnych możliwości ma repozytorium.
Wygodny schemat edukacyjny (właśnie jako schemat „warstw", a nie jako obietnica dokładnej hierarchii dziedziczenia wszystkich wersji) wygląda tak:
flowchart TD
A["CrudRepository
minimalny kontrakt CRUD"] --> B["PagingAndSortingRepository
+ sortowanie i zwracanie porcjami"]
B --> C["JpaRepository
+ wygody zorientowane na JPA"]
W naszym projekcie shop-data-jpa świadomie budujemy projekt JPA na PostgreSQL, więc logicznie jest patrzeć na te interfejsy przez pryzmat: który z nich sprawia, że repozytorium jest najbardziej wygodne i przewidywalne właśnie dla encji JPA.
3. CrudRepository: minimalny kontrakt „umiem przechowywać i usuwać"
CrudRepository to najbardziej „bazowy" kontrakt repozytoryjny w Spring Data. Wygodnie traktować go jako „niezbędne minimum", które odpowiada za cztery działania znane każdemu z CRUD: utwórz, przeczytaj, zaktualizuj, usuń. Tu ważne jest, żeby nie starać się zapamiętać całej listy metod od razu, lecz uchwycić ideę: CrudRepository to repozytorium, które gwarantuje, że masz operacje bazowe, ale nie obiecuje żadnego komfortu JPA.
Poniżej — przykład repozytorium dla Category, ale na minimalnym interfejsie. To nie to, co wybierzemy jako baseline kursu, ale to dobry sposób, żeby zobaczyć „dolną warstwę" mapy.
package com.example.shopdatajpa.catalog.repository;
import com.example.shopdatajpa.catalog.entity.Category;
import org.springframework.data.repository.CrudRepository;
// Kontrakt repozytorium dla encji Category na najbardziej minimalnym poziomie Spring Data.
public interface CategoryCrudRepository extends CrudRepository<Category, Long> {
// Category — typ encji, Long — typ klucza głównego (ID).
// Dostępne są tu bazowe operacje CRUD (save/findById/delete/existsById itp.).
}
Jeśli otworzysz dokumentację lub autouzupełnianie IDE, zobaczysz metody takie jak save(...), findById(...), existsById(...), findAll(), count(), delete(...). Dla projektu edukacyjnego i dla początkującego to już wygląda jak „wow, nie napisałem SQL, a to działa". I tak, to wow — ale bez magicznego myślenia: za tym stoi JPA i Hibernate, które generują SQL.
Jest jedna praktyczna subtelność, którą wiele osób zauważa dopiero po czasie: w bazowym kontrakcie często pojawia się Iterable<T>, a nie List<T>. To nie błąd, to po prostu „minimalność". Ale w kodzie aplikacyjnym List jest zazwyczaj wygodniejszy, bo chcesz znać rozmiar, indeksować elementy, sensownie streamować i po prostu nie pisać zbędnych konwersji.
4. PagingAndSortingRepository: CRUD + czytanie list
PagingAndSortingRepository to następna warstwa nad bazowym CRUD. Staje się zauważalny, gdy projekt przestaje być zabawką z trzema rekordami i zaczynasz myśleć tak, jak myśli backend developer w rzeczywistości: dane trzeba czytać nie w całości, a porcjami, i zazwyczaj w pewnym porządku. Nawet jeśli dziś nie budujesz pełnego katalogu z filtrami, sam fakt „sortowanie i zwracanie porcjami — część kontraktu repozytorium" jest bardzo ważny.
Pojawiają się tu metody przyjmujące Sort i Pageable. Nie będziemy teraz szczegółowo omawiać tych typów i ich zachowania, ważne jest żebyśmy zobaczyli: ten interfejs rozszerza możliwości repozytorium w kierunku „czytanie list".
package com.example.shopdatajpa.catalog.repository;
import com.example.shopdatajpa.catalog.entity.Category;
import org.springframework.data.repository.PagingAndSortingRepository;
// Repozytorium tej samej domeny, ale z rozszerzonym kontraktem czytania list:
// można pobierać dane z sortowaniem i paginacją.
public interface CategoryPagingRepository
extends PagingAndSortingRepository<Category, Long> {
// Ważna różnica od CrudRepository:
// pojawiają się metody takie jak findAll(Sort) i findAll(Pageable),
// żeby oddawać dane schludnie (porządek + porcje).
}
Dlaczego w ogóle warto to wiedzieć, skoro nie zagłębiamy się jeszcze w paging? Bo wielu nowicjuszy wybiera CrudRepository, a potem szczerze się dziwi, że „jakoś nie ma metod do sortowania i stron". A druga skrajność — próba dodawania „domowych" metod sortowania w serwisie, choć to dokładnie zadanie, które Spring Data już rozwiązuje na poziomie kontraktu.
W kategoriach zdrowego rozsądku: PagingAndSortingRepository to wciąż ogólny interfejs Spring Data, nie specyficzny dla JPA, ale już mówi: „repozytorium umie nie tylko przechowywać, ale i oddawać dane schludnie".
5. JpaRepository: baseline dla projektu JPA
JpaRepository to interfejs stworzony specjalnie dla świata JPA. Jego sens nie polega na tym, że jest „najfajniejszy", a na tym, że jak najbardziej naturalnie pasuje do typowego projektu Spring Boot + JPA. Zawiera bazowy CRUD, możliwości sortowania i czytania list, plus dodaje wygody zorientowane na JPA. Dla projektu edukacyjnego to też ważna zaleta metodyczna: mniej „nierówności" i mniej nieoczekiwanych ograniczeń w API.
W projekcie shop-data-jpa chcemy, żeby repozytoria wyglądały identycznie we wszystkich paczkach feature-owych: w catalog, później w inventory, następnie w ordering. Dlatego baseline będzie wyglądał tak:
package com.example.shopdatajpa.catalog.repository;
import com.example.shopdatajpa.catalog.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
// Równy baseline dla projektu JPA:
// wewnątrz jest już CRUD + paging/sorting + metody zorientowane na JPA.
public interface CategoryRepository extends JpaRepository<Category, Long> {
// Tutaj domyślnie więcej wygód niż w CrudRepository:
// często zwracane są List, są metody specyficzne dla JPA jak getReferenceById(...).
}
I tutaj pojawia się bardzo „przyziemiony" powód, dla którego JpaRepository jest wygodniejszy w kodzie aplikacyjnym: wiele metod zwraca List<T>, a nie tylko Iterable. Kiedy piszesz kod serwisu, to zauważalnie zmniejsza ilość drobnego „szumu", który nie uczy cię JPA, a uczy cię cierpliwości.
Na przykład metody findAll() z JpaRepository najczęściej używasz jako listy, a nie jako abstrakcyjnego iteratora:
import com.example.shopdatajpa.catalog.entity.Category;
import com.example.shopdatajpa.catalog.repository.CategoryRepository;
import java.util.List;
public class CatalogAdminReadService {
private final CategoryRepository categoryRepository;
public CatalogAdminReadService(CategoryRepository categoryRepository) {
// Wstrzykiwanie zależności: serwis zależy od repozytorium jako kontraktu dostępu do danych
this.categoryRepository = categoryRepository;
}
public List<Category> loadAllCategories() {
// Ważne: w JpaRepository findAll() zwraca List, a nie Iterable.
// W kodzie aplikacyjnym jest to zazwyczaj prostsze i bardziej czytelne.
return categoryRepository.findAll();
}
}
Warto odnotować oddzielnie: JpaRepository daje metody zorientowane na JPA, których w ogólnym kontrakcie nie musisz mieć. Na przykład metoda getReferenceById(...) (będziemy mówić o jej sensie osobno, tu ważny jest tylko fakt jej obecności) — to część API zorientowanego na JPA, i właśnie dlatego w lekturach i przykładach dotyczących JPA częściej widzisz JpaRepository niż CrudRepository.
6. Zasada wyboru: standaryzować baseline
Gdy wybierasz bazowy interfejs, wybierasz tak naprawdę nie tylko zestaw metod, ale styl projektu. W małym kodzie różnica wydaje się kosmetyczna, ale w prawdziwym projekcie repozytoriów będzie dużo, serwisów będzie dużo, i mózg potrzebuje jednego zrozumiałego standardu. W naszym projekcie edukacyjnym standard jest szczególnie ważny: budujemy kod „przykładowy", gdzie student powinien mniej zgadywać, a więcej rozumieć.
Przydatne jest spojrzenie na wybór przez prostą tabelę porównawczą. To nie „ściągawka do zapamiętania", a mapa, żeby się nie zgubić.
| Interfejs | Idea | Co daje „ponad" | Kiedy wygląda stosownie | Dlaczego w kursie wybieramy/nie wybieramy |
|---|---|---|---|---|
| CrudRepository | minimalny CRUD | operacje bazowe i API zorientowane na Iterable | gdy potrzebujesz ultra-minimum lub piszesz kod pod różne magazyny bez specyfiki JPA | jako baseline kursu jest niewygodny: dużo drobnych niedogodności i mniej API zorientowanego na JPA |
| PagingAndSorting Repository | CRUD + czytanie list | dodaje sortowanie i zwracanie porcjami | gdy chcesz dokładnie tej warstwy i nie chcesz specyfiki JPA | w projekcie JPA zazwyczaj nie ma sensu brać „środka", jeśli i tak żyjemy w JPA |
| JpaRepository | wygodny baseline JPA | wygodniejsze kolekcje i API JPA | typowy projekt Spring Boot + Spring Data JPA | nasz praktyczny default: równy kontrakt, mniej niespodzianek, łatwiejszy do czytania kod |
Najważniejsza zasada metodyczna na tym etapie: w edukacyjnym projekcie JPA prawie zawsze opłaca się wybrać JpaRepository jako jedyny baseline. To sprawia, że kod jest przewidywalny: otwierasz dowolne repozytorium i widzisz ten sam „pilot".
7. Nie mieszaj interfejsów bez powodu
Jeśli w jednym bloku feature-owym jedno repozytorium masz na CrudRepository, a drugie na JpaRepository, projekt „nie wybuchnie". Po prostu stanie się niewygodny. To subtelna, ale ważna różnica: kod będzie się kompilował, ale czytanie go stanie się nieprzyjemne, a uczenie się na nim — jeszcze nieprzyjemniejsze. Szczególnie cierpi warstwa serwisów, bo to miejsce, gdzie spotyka się kilka repozytoriów.
Na przykład zrobiłeś tak: CategoryRepository — na minimalnym interfejsie, a ProductRepository — na JpaRepository. W serwisie nagle dostajesz różne style API. Gdzieś zwracany jest Iterable, gdzieś List, gdzieś masz dostęp do wygodnych metod JPA, a gdzieś nie. I serwis zaczyna obrastać „owijkami", które nie mają nic wspólnego z logiką biznesową.
Poniżej mały przykład „szumu", który pojawia się, gdy niespodziewanie dostajesz Iterable, a chcesz zwykłej listy. To nie katastrofa, ale to też nie to, czemu chcemy poświęcać uwagę w kursie o danych.
import com.example.shopdatajpa.catalog.entity.Category;
import com.example.shopdatajpa.catalog.repository.CategoryCrudRepository;
import java.util.ArrayList;
import java.util.List;
public class CategoryReadService {
private final CategoryCrudRepository categoryRepository;
public CategoryReadService(CategoryCrudRepository categoryRepository) {
// Serwis zależy od węższego kontraktu (CrudRepository),
// więc dalej w kodzie będzie więcej „ręcznej" roboty.
this.categoryRepository = categoryRepository;
}
public List<Category> loadAsList() {
// Chcemy zwykłej listy, ale findAll() zwraca Iterable,
// więc trzeba ręcznie przenosić elementy do List.
List<Category> result = new ArrayList<>();
categoryRepository.findAll().forEach(result::add); // Zbędna robota przez zbyt wąskie API
return result;
}
}
Czy można tak żyć? Można. Czy trzeba tak żyć w projekcie, gdzie celem jest zrozumienie JPA, a nie „jak zamienić Iterable w List"? Zazwyczaj nie. Dlatego w ramach shop-data-jpa wybieramy równy baseline i nie zmuszamy kodu serwisu do zajmowania się tym, co nie należy do use case'u.
8. Rozwiązanie dla shop-data-jpa
Gdy tworzysz repozytoria w projekcie według zasady package-by-feature, pojawiają się naturalne miejsca na kontrakty dostępu do danych. Dla katalogu to com.example.shopdatajpa.catalog.repository, i tam mieszkają CategoryRepository i ProductRepository. I właśnie tu ważne jest, żeby kod był jak najbardziej „nudny" — w dobrym sensie: jednakowa baza, jednakowy styl, minimum niespodzianek.
Tak powinny wyglądać nasze repozytoria na tym etapie: są krótkie, czytelne i nie próbują odgrywać „architektonicznego wyczynu".
package com.example.shopdatajpa.catalog.repository;
import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
// Repozytorium katalogu na kontrakcie baseline JpaRepository:
// na tym etapie wystarczy nam standardowy zestaw metod.
public interface ProductRepository extends JpaRepository<Product, Long> {
// Product — encja, Long — typ ID.
// Własne zapytania i metody dodajemy dopiero gdy pojawia się realna potrzeba.
}
Zwróć uwagę na ważny detal: nie dodajemy tu „serwisowych" metod w stylu createCategoryAndRenameProduct(...). Repozytorium to kontrakt dostępu do danych jednej części modelu. Im czystszy ten kontrakt, tym łatwiej będzie napisać normalny CatalogService, gdzie będą mieszkały kroki scenariusza i walidacje.
9. Typowe błędy przy wyborze interfejsu repozytorium
Błąd nr 1: wybierać interfejs „po znajomej nazwie", a nie po roli.
Bardzo częsta historia: nowicjusz gdzieś widział CrudRepository, więc wszędzie stawia go „jako standard". Albo odwrotnie — słyszał słowo JpaRepository i stawia go w projekcie, który w ogóle nie jest o JPA. Prawidłowe podejście jest takie: najpierw rozumiesz, w jakim jesteś świecie (u nas JPA + PostgreSQL), a dopiero potem wybierasz bazowy kontrakt pod ten świat.
Błąd nr 2: mieszanie CrudRepository i JpaRepository w jednym projekcie bez powodu.
Mieszanie tworzy „nierówne" API. W jednym miejscu zwracany jest List, w innym Iterable, w jednym miejscu dostępne są metody zorientowane na JPA, w innym nie. W rezultacie warstwa serwisów zaczyna kompensować niespójność repozytoriów, i kod traci edukacyjną przejrzystość. Znacznie zdrowiej jest wybrać jeden baseline i utrzymywać go stabilnie.
Błąd nr 3: próba „nauczenia się wszystkich metod wszystkich interfejsów" od razu.
To pułapka perfekcjonizmu. Repozytorium ma wiele metod, a większość z nich nie będziesz używać codziennie. Na tym etapie ważniejsze jest nauczenie się czytania mapy: „minimalny CRUD", „dodane sortowanie/porcjowanie", „wygoda zorientowana na JPA". Szczegóły konkretnych metod omawiamy z sensem, gdy zaczynają być używane w kodzie serwisu.
Błąd nr 4: oczekiwanie możliwości specyficznych dla JPA od zbyt bazowego interfejsu.
Czasem student wybiera CrudRepository, a potem chce wywołać metodę JPA, której tam nie ma, i zaczyna myśleć, że „Spring Data się posypał". Spring Data się nie posypał — po prostu wybrałeś inny kontrakt. Jeśli projekt jest projektem JPA, JpaRepository zazwyczaj usuwa ten ból, bo zawiera bardziej „natywny" dla JPA zestaw możliwości.
Błąd nr 5: zamienianie repozytorium w „główny serwis aplikacji".
Wybór bazowego interfejsu nie ma tu znaczenia, ale błąd pojawia się w okolicach: skoro repozytorium wygląda potężnie, chce się tam wpchać całą logikę. Prowadzi to do giant interface, gdzie pojawiają się metody na poziomie use case'u. Repozytorium powinno pozostać kontraktem dostępu do danych, a scenariusze i kroki biznesowe powinny mieszkać w warstwie service. Im równiejszy i bardziej zrozumiały baseline repozytoryjny, tym łatwiej utrzymać tę granicę.
GO TO FULL VERSION