Spring Data JPA opisujemy szeroko w kursach dostępnych na naszym portalu, na przykład w rozdziale
Spring Data JPA: Zapytania Wbudowane (Built-in Queries).
Tam opisujemy jednak głównie podstawowe przypadki użycia. Teraz chcemy natomiast przedstawić bardziej złożony przykład. Polega on na przedstawieniu jednego zapytania,
składającego się z kilku różnych typów selektorów (również tych mniej lubianych przez programistów), co w efekcie ma doprowadzić do zwrócenia oczekiwanego zbioru danych.
Encje i relacje
Na początek kilka słów o relacjach. Mamy dwie encje:
Item i
Category, które są powiązane relacją
@ManyToMany. Encja
Item zawiera kolekcję aliasów dla nazw -
@ElementCollection.
Dodatkowo mamy też trzecią encję
User, w ramach której interesuje nas pole
firstName.
Spring Data JPA - Zapytanie wbudowane
Następnie mamy zdefiniowane zapytanie Spring Data JPA (wbudowane) operujące na encjach:
Zapytanie jest dość długie dlatego przyjrzyjmy się co dokładnie robi:
- findBy - wyszukuje encje Item po odpowiednio zdefiniowanych polach, łącząc kolejne warunki za pomocą And i Or
- NameLike - wybiera encje, których pole name jest zgodne z parametrem name metody
- DateTimeFromBefore - wybiera encje, których data z pola dateTimeFrom jest wcześniejsza, niż ta z parametru dateTimeFrom w metodzie
- NameAliasesContaining - wybiera encje, które w liście aliasów nameAliases zawierają alias o nazwie zgodnej z parametrem nameAlias w metodzie
- AppaCategories_idEquals - wybiera encje, które w liście kategorii mają kategorię o id równym parametrowi categoryId w metodzie
- Creator_firstNameEquals - wybiera encje, które posiadają encję User, której pole firstName równe jest parametrowi firstName
w metodzie
Od razu widać, że zapytanie nie należy do najprostszych. Tworzenie zapytań Spring Data JPA na podstawie wbudowanych słów kluczowych często przysparza problemy, jeśli chcemy
porównywać parametry metod z własnościami powiązanych obiektów, a w szczególności kolekcji. Dlatego warto raz jeszcze zwrócić uwagę na sposób wyszukiwania powiązania do
id kategorii, zaszytego wśród obiektów należących do kolekcji w encji:
To jednak nie wszystko. Zwróćmy uwagę na to jak zostanie potraktowane
And i
Or w zapytaniu.
Oczywiście zwrócone zostaną encje spełniające wszystkie kolejne warunki przed i po każdym
And lub ten jeden ostatni warunek występujący po
Or.
Zmiana wymagań klienta
Wszystko będzie dobrze jeśli takie jest nasze zamierzenie i faktycznie chcemy doprowadzić do takiego porównania. Co by jednak było gdybyśmy chcieli w tym zapytaniu
sprawdzić wszystkie warunki przed i po każdym
And, aż do ostatniego, a ten ostatni
And potraktować jako
sprawdzenie dwóch warunków rozdzielonych za pomocą
Or? A więc chcielibyśmy wykonać coś na wzór takiego pseudozapytania:
Wtedy sprawa się komplikuje. Niestety nie można stworzyć dokładnie takiego zapytania jak powyższe. Można ewentualnie próbować rozbijać to zapytanie na kilka zapytań
i później dodatkowo agregować, bądź tworzyć rozbudowaną hybrydę dwóch zapytań w jednym, gdzie jedno będzie miało zapewnioną zgodność z kategoriami, a drugie z imieniem - oba połączone
słowem
Or. Niemniej takie coś nawet bez głębszej analizy wydaje się rozwiązaniem absurdalnym.
W tym miejscu można się jeszcze ewentualnie zastanowić, czy naprawdę gdzieś w realnym systemie może zaistnieć sytuacja, aby trzeba było budować takie zapytanie?
Otóż każdy doświadczony programista pewnie potwierdzi, że jeśli coś w systemach informatycznych wydaje się prawie nierealne do wykonania (popularne nie da się),
to właśnie takie coś będziemy zmuszeni napisać, aby spełnić wymagania biznesowe.
A teraz spójrzmy na to z jeszcze innej strony. Mamy pierwotne zapytanie, które działa i wykonuje swoje zadanie przez pierwsze pół roku pracy systemu.
Niemniej klient chciałby w tej materii drobnego usprawnienia - żeby zamiast warunku "a" i "b" lub "c" warunek wyglądał tak jak napisaliśmy - "a" i ("b" lub "c").
Tak więc stajemy oko w oko z zadaniem przerobienia kodu, aby spełniał nowe wymaganie. Myślimy więc...
"zaadaptuję lekko zapytanie i po sprawie". Patrzymy do kodu i wtedy zaczyna się... Jakoś tak na siłę próbujemy nadal zrobić coś zapytaniem wbudowanym czego się
tym zapytaniem sensownie zrobić nie da. Internet już przeszukany, czas stracony. No nie da się. Co wtedy?
Spring Data JPA Custom Query - Zapytanie własne
Wtedy wystarczy zmienić koncepcję. Co prawda radykalne metody zwykle odkładamy w czasie na bliżej nieokreśloną przyszłość, ale w tym przypadku naprawdę się to opłaca.
Tworzymy zatem zapytanie za pomocą interfejsu
Query i problem rozwiązany:
Przyjrzyjmy się dokładnie temu co napisaliśmy. Wyjdzie nam z tego, że powyższe zapytanie robie dokładnie to o co nam chodziło.
- from Item i join i.appaCategories ac - wiąże encje Item z encjami Category (pole nazywa się appaCategories w encji) i używając słowa select wybiera rekordy spełniające określone warunki, występujące po where
(podobnie jak w SQL)
- i.name like :name - wybiera encje, których pole name jest zgodne z parametrem name metody
- i.dateTimeFrom < :dateTimeFrom - wybiera encje, których data z pola dateTimeFrom jest wcześniejsza, niż ta z parametru dateTimeFrom w metodzie
- :nameAlias member of i.nameAliases - wybiera encje, które w liście aliasów nameAliases zawierają alias o nazwie zgodnej z parametrem nameAlias w metodzie
- ac.id = :categoryId - wybiera encje, które w liście kategorii mają kategorię o id równym parametrowi categoryId w metodzie
- i.creator.firstName = :firstName - wybiera encje, które posiadają encję User, której pole firstName równe jest parametrowi firstName
w metodzie
-
and (...or...) - dwa ostatnie warunki traktuje wspólnie (jeden z nich musi być spełniony aby całość była spełniona)
No dobrze. Pytanie powinno jakie powinno być postawione w tym momencie polega na tym, dlaczego od początku nie używamy zapytań z interfejsem
Query. I to wydaje się być dobrze postawione pytanie.
Sami preferujemy właśnie to rozwiązanie, gdyż jest ono najbardziej elastyczne (może poza zapytaniami natywnymi, ale to już inna historia).
Czasami jednak w zadaniach, które
szczególnie na początku wydają sie mało skomplikowane, jak np. pobieranie użytkownika po adresie email, łatwiej jest użyć zapytań opartych o wbudowane słowa kluczowe.
Jest to szybsze i bardziej czytelne. Problem pojawia się wtedy, gdy system się rozwija i nawet najprostsze zapytania rosną w tempie ekspresowym.
I właśnie na taką sytuację możemy natrafić w projekcie, do którego właśnie dołączyliśmy. Stąd też warto wiedzieć jak to samo zrobić na kilka sposobów. Nigdy nie wiadomo co się nam przyda.
Na koniec zobaczmy jak można podejść do problemu z nieco innej strony. Zaimplementujemy zapytanie w sposób typowo programistyczny. Użyjemy Spring Data JPA Specification.
Spring Data JPA - Interfejs Specification
Na samym początku tworzymy obiekt specyfikacji:
W ramach specyfikacji tworzymy predykaty, które muszą być spełnione, aby encja mogła zostać zwrócona przez zapytanie.
Przykład jest o tyle fajny, że od razu prezentuje nam kilka różnych rodzajów predykatów:
- like - pole name encji jest zgodne z parametrem name metody
- lessThan - wartość w polu dateTimeFrom encji jest mniejsza, niż wartość parametru dateTimeFrom w metodzie
- isMember - wartość parametru nameAlias metody zawiera się w kolekcji nameAliases w encji
- joinList + equal - najpierw wskazujemy jaki rodzaj złączenia ma być użyty (INNER), a następnie określamy predykat tak, by wartość parametru categoryId była równa wartości id jednej z kategorii (spośród kategorii dołączonych do encji)
- get zagnieżdżonego obiektu + get pola - najpierw pobieramy (po nazwie pola) zagnieżdżoną encję User, a następnie określamy predykat tak, by wartość parametru firstName była równa wartości firstName z zagnieżdżonego obiektu
- or(p4, p5) - predykat polega na wyborze jednej z opcji
Ostatecznie mamy wiec sześć predykatów, które są budowane za pomocą
criteriaBuilder. Wszystkie te predykaty określają sposób w jaki chcemy wyciągać dane.
Dodatkowo na końcu łączymy wszystko w jeden "ostateczny" predykat, który polega na wykonaniu operacji
and. Oznacza to, że wszystkie predykaty muszą wystąpić
w tym samym czasie i tylko wtedy encja będzie zwrócona przez zapytanie. Dzięki możliwości łączenia warunków pięknie poradziliśmy sobie z zadaniem obsłużenia warunku "a" i ("b" lub "c").
Jak wykonać zapytanie z tak przygotowaną specyfikacją? Wystarczy, że interfejs naszego repository (
ItemRepository) będzie rozszerzał interfejs
JpaSpecificationExecutor<Item>.
Wówczas uzyskamy dostęp do kilku metod używających interfejsu
Specification. Możemy wtedy nasze zapytanie wywołać na przykład tak:
Zapytanie Spring Data JPA oparte o interfejs
Specification umożliwia - podobnie jak poprzednie konstrukcje - wykonanie takiego zapytania zarówno z obsługą stronicowania, jak i samego sortowania.
Autor: Jarek Klimas
Data: 12 stycznia 2019
Labele:Backend, Spring, Spring Data JPA, Poziom średniozaawansowany
Masz pytanie odnośnie zagadnienia omawianego w artykule?
Coś, co napisaliśmy, nie zaspokoiło Twojego głodu wiedzy?
Daj nam znać co myślisz i skomentuj artykuł na facebooku!