15 czerwca 2007

Dynamiczne zapytania i selekcja poprzez przykład czyli Hibernate nokautuje JPA

Najprościej i najwydajniej byłoby gdyby wszystkie zapytania, czy to SQL, HQL czy JPQL mogły być statyczne. Niestety, kiedy aplikacja umożliwia wyszukiwanie obiektów poprzez określenie tylko niektórych z możliwych do określenia wartości musi ona używać zapytań dynamicznych. Banalnym przykładem może być formatka wyszukiwania klienta, która umożliwia wyszukiwanie po numerze identyfikacyjnym, nazwisku, adresie, numerze telefonu itd., przy czym oczywiście możemy podać dowolną z tych wartości lub też dowolną ich kombinację. Uruchomienie takiego wyszukiwania koniec końców zakończyłoby się wygenerowaniem zapytania SQL w stylu:

SELECT * FROM Customer
WHERE firstName = 'Jan' AND phone = '504 101 102' AND street LIKE '%Słowackiego%'

a problem w tym, że nie wiemy a priori które właściwości klienta umieścić w klauzuli WHERE. Można by umieścić wszystkie właściwości, zamiast równości używać operatora LIKE a w miejsce nie podanych wartości wstawiać maskę wyszukiwania %, ale co z wydajnością takiego zapytania, zwłaszcza gdy pełne wyszukiwanie obejmowałoby kilka złączonych tabel? Zdaje się, że większość aplikacji nie obejdzie się bez zapytań dynamicznych. Jak więc je tworzyć? Najprostsze, najżmudniejsze, po prostu najgorsze, ale niestety czasami jedyne dostępne rozwiązanie polega na mozolnym zlepianiu treści zapytania w gąszczu instrukcji warunkowych. Dopóki jesteśmy w świecie JDBC, gdzie wszystko jest właśnie takie bez trudu akceptujemy ten balast, ale my nie chcemy by to był nasz świat i dlatego idziemy dalej.

Hibernate oferuje zgrabne rozwiązanie problemu dynamicznych zapytań udostępniając Hibernate Criteria API. Weźmy dla przykładu poniższe klasy:

public class Company {
private String name;

private Person representative;

// metody pomijam
}

public class Person {
private String firstName;

private String lastName;

// metody pomijam
}

Nie będę tu pisał podręcznika do Hibernate bo tych napisano już sporo. Poprzestanę na pokazaniu przykładów niezbędnych do kontynuowania artykułu. Załóżmy, że mamy obiekt klasy Company o nazwie templateCompany utworzony przez formatkę wyszukiwania firm. Kod realizujący dynamiczne wyszukiwanie firm spełniających zadane kryteria mógłby wyglądać tak:

Criteria criteria = session.createCriteria(Company.class)
.createAlias("representative", "rep");

if(templateCompany.getName() != null)
criteria.add(Restrictions.eq("name", templateCompany.getName()));

if(templateCompany.getRepresentative().getFirstName() != null)
criteria.add(Restrictions.eq("rep.firstName",
templateCompany.getRepresentative().getFirstName()));

if(templateCompany.getRepresentative().getLastName() != null)
criteria.add(Restrictions.eq("rep.lastName",
templateCompany.getRepresentative().getLastName()));

List<Company> companies = criteria.list();

Już jest nieźle, ale można całkiem fajnie, z użyciem mechanizmu selekcji poprzez przykład (ang. query by example). Ideą mechanizmu selekcji poprzez przykład jest, aby wyszukiwać obiekty nie poprzez uruchomienie zapytania, ale poprzez podanie obiektu przykładowego, stanowiącego wzorzec. Po zastosowaniu tej techniki powyższy przykład upraszcza się do:

Criteria criteria = session.createCriteria(Company.class)
.add(Example.create(templateCompany))
.createCriteria("representative")
.add(Example.create(templateCompany.getRepresentative()));

List<Company> companies = criteria.list();

Domyślnie działa to w ten sposób, że pod uwagę brane są tylko te właściwości obiektów używanych jako wzorzec, które są różne niż null. Trzeba jeszcze dodać, że powyższe mechanizmy można ze sobą dowolnie mieszać i łączyć. Muszę przyznać, że Hibernate mile mnie zaskoczył. Mechanizm jest naprawdę fajny, aczkolwiek niestety nie idealny. Rozszerzmy nieco nasz przykład, dodajmy kolejną klasę oraz jedną właściwość do klasy Company:

public class Company {
private String name;

private Person representative;

private Address address;

// metody pomijam
}


public class Address {
private String street;

private String city;

// metody pomijam
}

Jak tu teraz napisać kod umożliwiający wyszukiwanie firm także po adresie? Stosując podejście pierwsze, nieco rozwlekłe, wygląda to tak:

Criteria criteria = session.createCriteria(Company.class)
.createAlias("representative", "rep")

.createAlias("address", "address");

if(templateCompany.getName() != null)
criteria.add(Restrictions.eq("name", templateCompany.getName()));

if(templateCompany.getRepresentative().getFirstName() != null)
criteria.add(Restrictions.eq("rep.firstName",
templateCompany.getRepresentative().getFirstName()));

if(templateCompany.getRepresentative().getLastName() != null)
criteria.add(Restrictions.eq("rep.lastName",
templateCompany.getRepresentative().getLastName()));


if(templateCompany.getAddress().getStreet() != null)
criteria.add(Restrictions.eq("address.street",
templateCompany.getAddress().getStreet()));

if(templateCompany.getAddress().getCity() != null)
criteria.add(Restrictions.eq("address.city",
templateCompany.getAddress().getCity()));

List<Company> companies = criteria.list();

Kodu robi się sporo, ale przynajmniej mamy to, o co chodziło. Zawsze też można napisać jakąłś sprytną klasę pomocniczą. Niestety, stosując podejście drugie, selekcję poprzez przykład, taką jaką zaimplementowano ją w Hibernate Criteria API zrobić się tego po prostu nie da! I jest to tym bardziej drażniące, że wydaje się, iż wystarczyłaby drobna zmiana API. Mam cichą nadzieję, że jest to jednak moje a nie Hibernate’a przeoczenie i ktoś pokaże mi, że jednak to się zrobić da. No i wreszcie nokaut zapowiedziany w tytule artykułu. JPA nie oferuje żadnego takiego ani jakiegokolwiek innego mechanizmu wpierającego tworzenie dynamicznych zapytań. Problem ten stara się zaadresować projekt JPACriteria, http://jpacriteria.sourceforge.net/, ale jest on jeszcze we wczesnej fazie rozwoju.

7 komentarzy:

Waldek Kot pisze...

Cześć Mariusz,

Dobry artykuł - jedno sprostowanie odnośnie tego co nazywasz "nokautem". JPA oferuje mechanizm dynamicznego tworzenia zapytań. Nie jest to taki mechanizm jak Criteria API, ale pozwala dynamicznie stworzyć zapytanie i je wykonać. Czyli osiągany efekt jest ten sam (dynamiczne zapytanie) - inna jest droga dojścia do tego efektu. W JPA EntityManager ma metodę createQuery, do której przekazuje się zapytanie w postaci stringa.
Druga sprawa, to implementacje JPA - np. Toplink czy BEA Kodo - oferują rozszerzenia pozwalające tworzyć programowo dynamiczne zapytania oraz zadawać zapytania na zasadzie query-by-example. To są rozszerzenia JPA 1.0 - czyli tak samo jak jest to realizowane przez Hibernate (gdzie Criteria API jest rozszerzeniem JPA).
Tak jak napisałem wcześniej na pl.comp.lang.java - odpowiednik Criteria API ma się pojawić w Spring-u 2.1 i (co będzie prawdziwym nokautem) ma być dostępny dla wszystkich implementacji persystencji wspieranych przez Spring-a (czyli od JDBC, przez iBatis, po Hibernate, JDO czy JPA). Uważam, że miłe...

Pozdrawiam,
Waldek Kot

Mariusz Lipiński pisze...

Dzięki za komentarz, przyznam że mnie zaintrygowałeś, ale nie do końca rozumiem. Czy mechanizm tworzenia dynamicznych zapytań w JPA (chodzi mi o specyfikację a nie konkretny produkt) o którym piszesz to sklejanie stringa w taki sam sposób jak byśmy używali JDBC a potem createQuery z tym stringiem czy jest tam coś więcej? Chyba niestety tylko tyle.

Waldek Kot pisze...

Tak - w zakresie CZYSTEJ specyfikacji JPA to polega na odpowiednim przygotowaniu tego stringa (no może jeszcze to, że do tego stringa można przekazywać parametry - nazwane albo numerowane). Tym niemniej powstaje dynamiczne zapytanie (w Twoim artykule napisałeś, że w JPA ogóle nie ma mechanizmu tworzenia dynamicznych zapytań, stąd moje sprostowanie).
Z tym, że implementacje JPA (przynajmniej Oracle TopLink, BEA Kodo oraz JBoss Hibernate) oferują rozszerzenia, gdzie można query zbudować "programowo", a także dynamicznie określić dodatkowe jego parametry (np. "z podanego zapytania zwróć tylko wyniki od 27 do 38"). Podobnie, te implementacje mają możliwość query-by-example.

Pozdrawiam,
Waldek Kot

Mariusz Lipiński pisze...

Zgoda, JPA umożliwia budowanie dynamicznych zapytań, aczkolwiek nie wspiera tego w takim stopniu jak inne technologie, które wymieniasz. Zgoda, można używać rozszerzeń poszczególnych dostawców, tyle, że wtedy nie jest to już JPA. Osobiście lubię JPA i trzymam kciuki za następną wersję specyfikacji.

Waldek Kot pisze...

Co do drugiej zgody, czyli czy to jest wciąż JPA czy nie - to myślę, że w praktyce i tak nie da się uniknąć stosowania funkcjonalności wychodzących poza oficjalną specyfikację. Tzn. da się, ale koszt tego zwykle jest większy niż zysk. Ja jestem zdania, że jeśli komuś jakaś funkcjonalność wykraczająca poza standard jest potrzebna, to trzeba z niej korzystać. Warto oczywiście zrobić to rozsądnie, tak, żeby w miarę łatwo dało się wprowadzić ewentualne zmiany czy zrefaktoryzować. W przypadku JPA i dostawców JPA (przynajmniej tych którym się przyglądałem), to się daje w miarę łatwo zrobić (i te optymalizacje zacieśnić do wąskich miejsc w kodzie, ale wręcz opcji on/off w konfiguracji).

Jest to też jedno z zadań frameworków aplikacyjnych, aby niskopoziomowe API ładnie opakowywać, a wręcz "z pudełka" dawać typowe scenariusze.

Pozdrawiam,
Waldek Kot

Adam Wozniak pisze...

Cześć

Co tu dużo mówić - Criteria API z Hibernate jest zajebiste :) Brak tego mechanizmu w JPA bardzo obniża wartość technologii JPA w moich oczach (dlatego trzymam się cały czas Hibernate).

Ludzie interesujący się ORM dzielą się na dwie grupy. Jedni uważają, że należy stosować standardy (np. JPA), gdyż ma się wtedy wolność w wyborze dostawcy rozwiązania JPA i w każdej chwili można podmienić silnik JPA. Inni (w tym ja) uważają, że wybieranie standardów (tu: JPA) po to aby zapewnić sobie możliwość zmiany silnika JPA jest ... bez sensowne :> Należę do grupy, którą bardziej interesują możliwości silnika ORM, a nie możliwość zamieniania silników do persystencji.

To jest kolejny argument, dlaczego używam Hibernate. Z tego co wiem o silnikach ORM uznałem, ze Hibernate jest silnikiem ORM o największych możliwościach (jeśli ktoś zna silnik ORM silniejszy od Hibernate, to proszę o głos - bardzo mnie ten temat interesuje).

Mimo, ze bardzo mi się podobają Criteria API w Hibernate, to jednak brakuje mi pewnej rzeczy. Jednak brak tego mechanizmu wynika z braku pewnego eleganckiego mechanizmu w samej Javie. Nie chcąc się rozpisywać wklejam adres do mojego postu na forum Hibernate:
http://forum.hibernate.org/viewtopic.php?p=2321348&sid=f5fd791a9f261cd0e7cffa8ebda0909c

Pozdrowienia,
Adam

Sławomir Wojtasiak pisze...

Witam,

Mechanizm bardzo pomocny, ale niestety problemy pojawiaja się przy bardziej skomplikowanych zapytaniach, które wymagają złączeń ze wstępnym odczytywaniem danych( JOIN FETCH), z którymi implementacja Hibernate Criteria API nie za bardzo sobie radzi.

Pozdrawiam,
Sławomir Wojtasiak