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.

9 czerwca 2007

Zbędne złączenia w zapytaniach SQL generowanych przez TopLink JPA

To, co dzisiaj opiszę to efekt moich doświadczeń z pracy z TopLink Essentials 2.0-b47-beta3 jako dostawcą Java Persistence API 1.0. Nie weryfikowałem, czy inne implementacje zachowują się analogicznie ani czy na ten temat mówi cokolwiek specyfikacja, ale zważywszy, że jest to szczegół implementacyjny wydaje mi się to wysoce nieprawdopodobne. No więc do rzeczy. Zerknijmy na poniższe klasy, encje JPA:

@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long productId;

private String productDescr;

private ProductCategory productCategory;
}

@Entity
public class ProductCategory {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long categoryId;

private String categoryName;

@OneToMany(mappedBy = "productCategory", fetch = FetchType.LAZY)
private List products;
}

Nie istotne dla przykładu właściwości i metody pominąłem dla przejrzystości. Dla encji tych TopLink wygenerował następujące tabele i więzy w bazie danych:

CREATE TABLE PRODUCT (
PRODUCTID BIGINT NOT NULL,
PRODUCTDESCR VARCHAR(255),
PRODUCTCATEGORY_CATEGORYID BIGINT,
PRIMARY KEY (PRODUCTID)
)

CREATE TABLE PRODUCTCATEGORY (
CATEGORYID BIGINT NOT NULL,
CATEGORYNAME VARCHAR(255),
PRIMARY KEY (CATEGORYID)
)

ALTER TABLE PRODUCT
ADD CONSTRAINT FK_PRODUCT_PRODUCTCATEGORY_CATEGORYID
FOREIGN KEY (PRODUCTCATEGORY_CATEGORYID)
REFERENCES PRODUCTCATEGORY (CATEGORYID)

Napiszmy teraz zapytanie JPQL pobierające wszystkie produkty należące do danej kategorii. Oto pierwsza próba:

SELECT p FROM Product p WHERE p.productCategory.categoryId = :categoryId

No i w tym miejscu nabrałem wątpliwości. Czy aby na pewno JPA zadba o optymalizację? Czy zauważy, że złączenie z tabelą PRODUCTCATEGORY nie jest potrzebne do wyliczenia wyniku tego zapytania? TopLink we wspomnianej wersji nie zauważy i wygeneruje następujące zapytanie SQL:

SELECT t0.PRODUCTID, t0.PRODUCTDESCR, t0.PRODUCTCATEGORY_CATEGORYID
FROM PRODUCT t0, PRODUCTCATEGORY t1
WHERE (t1.CATEGORYID = ?) AND (t1.CATEGORYID = t0.PRODUCTCATEGORY_CATEGORYID)

Hmmm… czyżby implementacja takiej z pozoru prostej optymalizacji była trudna? Korci mnie żeby zajrzeć jak to implementują inni, ale póki co wygrywa brak czasu, tym bardziej że wystarczy zmienić nieco nasze zapytanie i będzie jak trzeba. Poprawiona, równoznaczna semantycznie wersja zapytania JPQL poniżej:

SELECT p FROM Product p WHERE p.productCategory = :prodCat

Zmiana wydaje się oczywista, aczkolwiek do uruchomienia takiego zapytania nie wystarczy już identyfikator kategorii, będziemy potrzebowali obiektu kategorii. Jest to trochę irytujące zwłaszcza, że obiekt ten posłuży li tylko jako opakowanie do identyfikatora, ale z drugiej strony ratuje sytuację, bowiem jako parametr zapytania wystarczy podać obiekt new ProductCategory(categoryId). Upewnijmy się jeszcze, że teraz jest dobrze. Rzeczywiście, wygenerowane zapytanie SQL to:

SELECT PRODUCTID, PRODUCTDESCR, PRODUCTCATEGORY_CATEGORYID
FROM PRODUCT
WHERE PRODUCTCATEGORY_CATEGORYID = ?

2 czerwca 2007

Serializacja do XML i deserializacja przy użyciu XStream

Po długiej i wyczerpującej, ale bardzo pouczającej fazie czytania, eksperymentowania i rozmyślania przystąpiłem wreszcie do pisania… pisania swojej pracy magisterskiej. Częścią tej że jest implementacja aplikacji WWW. Po sporządzeniu ogólnego projektu przystąpiłem do realizacji metodą z góry na duł (ang. top-down) tj. począwszy od interfejsu użytkownika stopniowo przesuwając się w stronę serwisów warstwy logiki i w końcu serwisów DAO (akr. Data Access Object). I tu pojawił się problem, którego rozwiązanie leży u podstaw tego artykułu, tj. pisanie tymczasowych klas zaślepek (ang. stub) serwisów pobierających dane. Zacząłem od wymarzenia sobie biblioteki, która wczytywałaby drzewo obiektów z pliku XML. Uznałem, że tak będzie najłatwiej. Chwile potem miałem już namierzone rozwiązanie, bibliotekę XStream, http://xstream.codehaus.org/. Załóżmy, że chcemy zbudować, docelowo poprzez pobranie z bazy danych, ale póki co najłatwiej jak się da, strukturę obiektów modelujących katalog produktów podzielonych na kategorie. W tym celu użyjemy klas:

public class ProductCategory {
private String categoryName;
private ProductCategory parentCategory;
private List<ProductCategory> childCategories;
private List<Product> products;

// metody pomijam
}

public class Product {
private String productName;
private String productDescr;

// metody pomijam
}

Programowe tworzenie wystarczających rozmiarów struktury tego typu byłoby nudne i męczące, łatwiej będzie napisać odpowiedni plik XML. W naszym przykładzie może on wyglądać tak:

<linked-list>
<pl.mariuszlipinski.ProductCategory id="1">
<categoryName>Komputery</categoryName>
<childCategories>
<pl.mariuszlipinski.ProductCategory>
<categoryName>Laptopy</categoryName>
<parentCategory reference="1" />
<products>
<pl.mariuszlipinski.Product>
<productName>ThinkPad T41</productName>
<productDescr>Fajna rzecz</productDescr>
</pl.mariuszlipinski.Product>
<pl.mariuszlipinski.Product>
<productName>Compaq nc6400</productName>
<productDescr>Dobra rzecz</productDescr>
</pl.mariuszlipinski.Product>
</products>
</pl.mariuszlipinski.ProductCategory>
</childCategories>
</pl.mariuszlipinski.ProductCategory>
</linked-list>

W jaki sposób XML ten odpowiada klasom widać gołym okiem, dopasowanie odbywa się po nazwach. Możliwe jest również definiowanie aliasów i to nawet za pomocą adnotacji, ale nie jest to szczególnie istotne. No więc jak ten plik XML wczytać? Jak zwykle trzeba zacząć od pobrania biblioteki ze strony projektu, ze strony http://xstream.codehaus.org/download.html. Najłatwiej będzie pobrać dystrybucję binarną, tj. jeden plik .zip zawierający także zależności, czyli parser XML. Sam XStream to plik xstream-(…).jar, rekomendowany parser Xpp3 to plik xpp3-(…).jar. To co teraz należy wykonać to deserializacja XML. Oto kod który realizuje to zadanie:

XStream xStream = new XStream();
xStream.setMode(XStream.ID_REFERENCES);

List<ProductCategory> catalog = xStream.fromXML(new FileReader("catalog.xml"));

To, co zasługuje w powyższym przykładzie to szczególną uwagę to referencje. Zwróćmy uwagę na fragment naszego XML’a:

<pl.mariuszlipinski.ProductCategory id="1">
<childCategories>
<pl.mariuszlipinski.ProductCategory>
<parentCategory reference="1" />

Wczytanie elementu <pl.mariuszlipinski.ProductCategory id="1"> spowoduje utworzenie obiektu klasy ProductCategory. Element childCategories spowoduje utworzenie i przypisanie do zmiennej tego obiektu o tej że nazwie obiektu implementującego interfejs List. Kolejny element <pl.mariuszlipinski.ProductCategory> spowoduje utworzenie nowego obiektu klasy ProductCategory i dodanie go do wspomnianej listy. I teraz uwaga. Element <parentCategory reference="1" /> spowoduje przypisanie do zmiennej parentCategory obiektu z atrybutem @id=”1”, a więc tego utworzonego w wyniku parsowania elementu <pl.mariuszlipinski.ProductCategory id="1">. XStream wspiera kilka różnych sposobów wyrażania referencji, i właśnie po to by użył tego opisanego powyżej wywołujemy metodę:

xStream.setMode(XStream.ID_REFERENCES);

Kolejnym ciekawym zastosowaniem dla deserializacji XML jest wczytywanie zaawansowanej konfiguracji. Po co ograniczać się do prostej mapy wartości możliwej do uzyskania przy użyciu plików .properties.

Jakiś czas temu, w artykule Implementacja metody toString() z użyciem Reflection API zaproponowałem uniwersalny kod implementujący metodę toString(). Używałem tej że do testowania serwisów DAO poprzez wypisanie odczytanych obiektów. W komentarzu do tamtego artykułu Michał zaproponował lepsze rozwiązanie, z użyciem klasy ReflectionToStringBuilder. Teraz znowu moja propozycja, serializacja do XML. Aby zserializować kompletne drzewo obiektów wystarczy poniższy kod:

XStream xStream = new XStream();

String xml = xStream.toXML(products);