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