Criteria API multiselect i różnice w implementacjach JPA

Tworzenie zapytań w JPA to rzecz zarazem łatwa jak i trudna. Łatwa, bo bardzo prosto jest stworzyć zapytanie oparte o zwykłego SQL’a lub JPQL’a, a nawet zapytania tworzone za pomocą Criteria API nie są czymś nadzwyczajnym. Zarazem jest to jednak trudny temat, bo trzeba pamiętać o wielu kwestiach związanych z wydajnością lub bezpieczeństwem, których pominięcie może spowodować spore problemy w przyszłości. Nie chcę tu się rozpisywać po trochu o wszystkim, a poruszyć jeden temat, który zaciekawił mnie szczególnie.

Jedną z ważniejszych rzeczy związanych z projektowaniem zapytań do bazy danych jest zawężanie zapytań w taki sposób, aby zwracały tylko niezbędne do konkretnego widoku aplikacji dane. Gdy używamy technologii ajaxowych (np. GWT), ogranicza to nam ilość przesyłanych danych do przeglądarki, a nawet jeśli nie, to mniej cierpi na tym serwerowy cache (jeśli go włączyliśmy). JPA dostarcza kilka rozwiązań, które umożliwiają zawężenie wyników zapytań, a ciekawsze z nich to Constructor Expression wykorzystane wraz z Named Queries, oraz multiselect w Criteria API. Ciekawsze, bo oba pozwalają zmapować zapytanie bezpośrednio na konstruktor klasy DTO. Dziś będzie o tylko o tym drugim, czyli o metodzie multiselect.

Lubię Criteria API za to, że jest silnie obłożone generykami i pozwala na pisanie kodu bez parametryzowania go przeróżnymi String’ami, szczególnie wtedy gdy używa się metamodel’i. Kod stworzony w taki sposób jest bardzo odporny na błędy powstałe wskutek refaktoringu modelu danych oraz łatwo poddaje się modyfikacjom. Instrukcja multiselect pozwala zaś na wybranie jedynie tych danych, których rzeczywiście potrzebujemy. Można więc stworzyć zapytanie, które wyciąga dane z kilku różnych encji (połączonych relacjami) i wpakować je od razu do jakiejś małej klasy DTO. Wygląda to tak, że do metody multiselect przekazujemy listę wyrażeń ze ścieżkami do pól encji i zostaną one zmapowane na konstruktor klasy DTO. Nad poprawnością mapowania nie czuwa jednak kompilator i o błędzie (lub jego braku) dowiemy się dopiero przy próbie wywołania takiego zapytania.

Poniżej przykład zapytania z wykorzystaniem multiselect:

CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<ListCarDTO> cq = cb.createQuery(ListCarDTO.class);
Root<CarEntity> r = cq.from(CarEntity.class);

cq.multiselect(r.get(CarEntity_.id), r.get(CarEntity_.model), r.get(CarEntity_.brand));

TypedQuery<ListCarDTO> tq = em.createQuery(cq);
List<ListCarDTO> result = tq.getResultList();

Powyższy przykład mapuje trzy atrybuty encji CarEntity i mapuje je na konstruktor klasy ListCarDTO. Docelowo dostajemy listę obiektów ListCarDTO tylko z danymi, których potrzebowaliśmy. Często wyniki zapytań i tak przepakowuje się do klas DTO przed ich wysłaniem przez serwis (ręcznie, czy wykorzystując dostępne mappery), a tu JPA robi to za nas.

Korzystając z multiselect’a od razu zauważyłem jedną ciekawą rzecz. Metoda ta przyjmuje listę lub tablicę obiektów implementujących interfejs javax.persistence.criteria.Selection a nie chociażby javax.persistence.criteria.Path. Czyli w teorii można tam przekazać np. warunki skonstruowane za pomocą CriteriaBuilder’a, które zwracają wartość boolean lub skonkatenować wartości kilku kolumn. Możliwości może być tu wiele i łatwo sobie wyobrazić ich zastosowania. Postanowiłem więc sprawdzić, czy moje obserwacje są słuszne i to na co pozwala API rzeczywiście będzie działać tak jak myślałem.
W praktyce okazało się, że tak różowo już nie jest, bo nie każdy dostawca JPA zaimplementował wszystko tak jak trzeba :) Jako że na co dzień używam Hibernate, to właśnie na nim testowałem multiselect’a pierwszy raz. Szybko okazało się, że Hibernate radzi sobie tylko z prostym wyciąganiem atrybutów encji, a jakiekolwiek dodatkowe operacje kończyły się niepowodzeniem. Trochę zawiedziony Hibernate’m (o czym będzie też niżej), pokusiłem się o mały test sprawdzający innych dostawców JPA. Zobaczmy więc, jak to faktycznie jest z deklarowaniem przez niektórych zgodności w 100% ze specyfikacją JPA 2.0.

Stworzyłem przykładowy projekt, który zawiera w sobie 5 prostych przypadków testowych wykorzystujących Criteria API multiselect i przepakowujących do klas DTO dane na podstawie różnych warunków:

  • 3 kolumny z encji
    cq.multiselect(
        r.get("id"),
        r.get("model"),
        r.get("brand"));
  • 3 kolumny + isNotNull (String)
    cq.multiselect(
        r.get("id"),
        r.get("model"),
        r.get("brand"),
        cb.isNotNull(r.get("vin"));
  • 3 kolumny + equal (Enum)
    cq.multiselect(
        r.get("id"),
        r.get("model"),
        r.get("brand"),
        cb.equal(r.get("color"), Color.BLUE));
  • 3 kolumny + greaterThan (Date)
    cq.multiselect(
        r.get("id"),
        r.get("model"),
        r.get("brand"),
        cb.greaterThan(r.<Date> get("produced"), currentCenturyDate);
  • 3 kolumny + concat (String)
    cq.multiselect(
        r.get("id"),
        r.get("model"),
        r.get("brand"),
        cb.concat(cb.concat(r.<String> get("model"), " "), r.<String> get("brand"));

W trakcie przygotowywania testowego projektu zrezygnowałem z obsługi metamodel’i (o czym wspomnę jeszcze pod koniec), więc wyrażenia tworzone są przy pomocy nazw atrybutów encji przekazanych jako String.

Za pomocą profili Maven’a skonfigurowałem 6-ciu różnych dostawców JPA 2.0. W każdym przypadku użyłem najnowszej stabilnej wersji dostępnej w dniu przygotowywania tego testu (12 listopada 2013):

  • Hibernate (4.2.7.Final)
  • EclipseLink (2.4.2)
  • OpenJPA (2.2.2)
  • DataNucleus (3.3.3)
  • Batoo JPA (2.0.1.2)
  • ObjectDB (2.5.3)

Aktualizacja 2013.11.20:

  • DataNucleus (3.3.4)

Poniżej zamieszczam wyniki testu:

 HibernateEclipseLinkOpenJPADataNucleusBatoo JPAObjectDB
Test 1: 3 kolumny
Test 2: 3 kolumny + isNotNull (String)
Test 3: 3 kolumny + equal (Enum)
Test 4: 3 kolumny + greaterThan (Date)
Test 5: 3 kolumny + concat (String)

Hibernate

Wynik Hibernate’a to chyba największe zaskoczenie tego testu. Hibernate cieszy się największą popularnością spośród wszystkich dostawców JPA oraz oferuje możliwości znacznie wykraczające poza oficjalną specyfikację. Okazało się jednak, że zaraz po DataNucleus’ie nie poradził sobie z największą ilością testów w zestawieniu. Zadziałała w zasadzie tylko konkatenacja String’ów (no i podstawowy test listujący wartości atrybutów encji), testy sprawdzające warunki stworzone za pomocą CriteriaBuilder’a nie wykonały się poprawnie. No cóż… spodziewałem się, że będzie lepiej…

EclipseLink

Referencyjny dostawca JPA od Oracle’a i fundacji Eclipse poradził sobie tylko nieznacznie lepiej od poprzednika. Tak jak w przypadku Hibernate’a, poprawnie wykonała się konkatenacja String’ów. Dodatkowo wykonał się również test sprawdzający istnienie wartości w określonej kolumnie – isNotNull. Niemniej ten dostawca do zaawansowanych operacji z wykorzystaniem multiselect również się nie nadaje.

OpenJPA

Tu pierwsze miłe zaskoczenie powyższego testu. OpenJPA długo było uważane za niezbyt stabilną implementację, nie nadającą się do krytycznego wykorzystania produkcyjnego. To się już jakiś czas temu zmieniło, a dodatkowo pod względem kompatybilności ze specyfikacją nie ma się do czego przyczepić. Do tej pory nie miałem jeszcze okazji wykorzystania OpenJPA w jakimkolwiek projekcie, ale pozytywny wynik testu sprawia, że na pewno w najbliższym czasie to się zmieni.

DataNucleus

DataNucleus, to najbardziej uniwersalny dostawca JPA w powyższym zestawieniu. Przy okazji jest również referencyjną implementacją JDO i jest szeroko stosowany w platformie Google App Engine. Pozwala na podpięcie różnych typów źródeł danych, oprócz relacyjnych baz danych, np. dokumentów Excel, XML, ODF, czy np. bazy LDAP. W parze z funkcjonalnością nie idzie jednak dokładność implementacji i DataNucleus okazał się najgorszym z testowanych produktów. Oprócz listowania atrybutów encji nie poradził sobie z żadnym testem, gdzie w grę wchodziło wyrażenie utworzone przez CriteriaBuilder’a.

Przy implementacji testów pojawiły się kolejne dwa drobne problemy (niezwiązane bezpośrednio z samym testem). Pierwszy: DataNucleus nie umożliwia tworzenia zapytań nazwanych bez aliasów. Nie sprawdzałem wszystkich przypadków, ale mi nie przeszła instrukcja usuwająca wszystkie rekordy z tabeli: „DELETE FROM SampleEntity”. Po dodaniu aliasu instrukcja JPQL zadziałała poprawnie. Drugi problem wystąpił podczas próby wygenerowania metamodel’u. Teoretycznie DataNucleus oferuje taką możliwość, mi jednak po wielu próbach nie udało mi się tego dokonać. Obejściem może być użycie innego generatora np. z Hibernate’a – metamodel’e są kompatybilne.

Aktualizacja 2013.11.10: Po zgłoszeniu błędu w systemie JIRA DataNucleus’a problem został rozwiązany. Obecnie DataNucleus wszystkie testy przechodzi poprawnie. Poprawka została zrealizowana dla wersji 3.3.4.

Batoo JPA

Drugie miłe zaskoczenie, tym większe, że Batoo JPA jest jedną z młodszych implementacji JPA, rozwijaną w dodatku przez jednego programistę. W założeniach ma być szybkie, lekkie i maksymalnie kompatybilne ze specyfikacją. Nie testowałem szybkości jego działania, jednak pod względem kompatybilności na pewno cel został osiągnięty (nie jestem w stanie tu potwierdzić poprawności działania w innych obszarach JPA). Batoo z pewnością kusi, by użyć go w jakimś małym projekcie.

Ciekawostka: przy implementacji testów Batoo zawieszał wątek, jeśli EntityManager nie był zamykany, a następowała próba utworzenia kolejnej jego instancji. Po poprawieniu kodu wszystko zaczęło działać, więc głębiej w temat nie wnikałem.

ObjectDB

Ostatni bohater zestawienia, to implementacja JPA (oraz JDO) dostarczana wraz z autorską bazą obiektową o tej samej nazwie. Rozwiązanie nieco niszowe, dla dużych komercyjnych projektów płatne, ale podobno diabelsko szybkie. Jak na komercyjny projekt przystało, ObjectDB bez problemu poradziło sobie ze wszystkimi testami. Darmowa licencja pozwala na pracę z maksymalnie 10-cioma encjami i milionem rekordów na plik bazy, więc dla małych projektów warto się nad tą implementacją zastanowić.

Minus jest taki, że obecnie ObjectDB nie obsługuje w ogóle metamodel’i (nawet wygenerowanych przez innych dostawców), przez co musiałem w ogóle zrezygnować z ich użycia w testowym projekcie. Podobno wkrótce ma się to zmienić, póki co po prostu metamodel’e nie działają i tyle.

 

Faworytami testu okazały się implementacje najmniej popularne. Trochę martwi, że duzi gracze (szczególnie Hibernate) trochę po macoszemu traktują sprawę zgodności ze specyfikacją. Cieszy, że ci mniejsi starają się nie zginąć i zadowolić przynajmniej pod niektórymi względami wybrednych programistów. Pełen podziwu jestem szczególnie dla implementacji Batoo JPA, o której jeszcze dwa dni temu nic nie słyszałem, a tak mile mnie zaskoczyła przechodząc wszystkie testy.

Źródła projektu testowego można znaleźć na repozytorium.

Skomentuj

4 Komentarze.

  1. Bardzo dobry wpis, rzetelny i profesjonalny opis zagadnienia. Dziękuję i pozdrawiam.
    Lagnesja ostatnio napisał(a)… Historyjki gumy Donald to było coś!

  2. zacząłem ostatnio się zajmować pisaniem w javie, dobrze że trafiłem na Twojego bloga i parę trików mogę poznać

  3. hmm szukam właśnie informacji na temat JPA i trochę mi rozjaśniłeś sprawę, czas przystąpić do projektu :twisted:

  4. Hibernate kompletnie zawiódł :shock: Szkoda.

Odpowiedz dla Lagnesja ¬
Anuluj odpowiedź


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge