9 kwietnia 2007

Niebanalny przykład mapowania dla Apache iBATIS Data Mapper

Nie trudno zauważyć przeglądając króciutką historię mojego bloga, nad czym ostatnio spędzam wieczory. Tematem przewodnim jest iBATIS Data Mapper, wszystkie wątki skupione są wokół technologii trwałości danych. Napisałem już dwa artykuły wprowadzające w tematykę Data Mapper’a: Wstęp do technologii Apache iBATIS Data Mapper oraz Uruchamiamy aplikację JPetStore dla Apache iBATIS Data Mapper, przyszła kolej na coś nieco bardziej zaawansowanego. Będzie przykład mapowania obiektu, który zawiera referencję do innego obiektu, który z kolei zawiera referencję do kolekcji obiektów. Całą strukturę wczytamy pojedynczym zapytaniem SQL. Żeby pokazać, że iBATIS potrafi.


Zacznę od przedstawienia klas języka Java i tabel bazy danych, w końcu to są aktorzy pierwszej kategorii. Oto klasy, których będziemy używali:

public class User {
private long id;
private RolesSet rolesSet;

// potrzebujemy także kompletu get’terów i set’terów
}

public class RolesSet {
private Set<String> roles = new HashSet<String>();

// potrzebujemy także get’tera i set’tera
}

public class Credentials {
private String login;
private String passwd;

// potrzebujemy także kompletu get’terów i set’terów
}

Naturalnie klasy te mogą, a nawet powinny zawierać dowolne inne metody i właściwości, ale pominąłem je dla zwięzłości wywodu. Tabele bazy danych przedstawię w formie używanej przez narzędzie DdlUtils, http://db.apache.org/ddlutils/, bardzo fajne, aczkolwiek słabo jeszcze rozwinięte:

<table name="credentials">
<column name="isValid" required="true" type="BIT" />
<column name="login" required="true" type="VARCHAR" size="64" />
<column name="passwd" required="true" type="VARCHAR" size="64" />
<column name="userId" required="true" type="INTEGER" />
</table>

<table name="role">
<column name="id" primaryKey="true" required="true" type="INTEGER" />
<column name="name" required="true" type="VARCHAR" size="32" />
</table>

<table name="userRoles">
<column name="isValid" required="true" type="BIT" />
<column name="userId" required="true" type="INTEGER" />
<column name="roleId" required="true" type="INTEGER" />
</table>

Prawda, że powyższe prezentuje się dużo ciekawiej niż tradycyjny skrypt? Tabele znacznie odchudziłem na potrzeby przykładu, nie koniecznie wyglądają więc teraz sensownie, ale spełnią swoją rolę. To, czego chcemy to skonstruować w pełni zainicjalizowany obiekt klasy User na podstawie obiektu klasy Credentials. Chcemy więc uruchomić coś w stylu:

User user = (User) sqlMapClient.queryForObject("getUser", credentials);

i w wyniku otrzymać obiekt user, taki że zbiór user.getRolesSet().getSet() jest w pełni zainicjalizowany. Wpierw wariant podstawowy, tj. taki, w którym w wyniku pierwszego zapytania otrzymujemy userId z tabeli credentials o ile oczywiście login i hasło są prawidłowe a następnie używamy drugiego zapytania, aby dla danego userId otrzymać zbiór nazw ról z połączonych tabel userRoles i role. Oto jak mógłby wyglądać nasz plik mapowania:

<resultMap class="pckg.User" id="getUserMap">
<result property="id" column="userId"/>
<result property="rolesSet.roles" column="userId" select="getRoles"/>
</resultMap>

<resultMap class="string" id="getRolesMap">
<result property="value" column="name"/>
</resultMap>

<select id="getRoles" parameterClass="long" resultMap="getRolesMap">
SELECT r.name
FROM userRoles u, role r
WHERE u.userId = #value# AND u.isValid = 1 AND r.id = u.roleId
</select>

<select id="getUser" parameterClass="pckg.Credentials" resultMap="getUserMap">
SELECT userId
FROM credentials
WHERE login = #login# AND passwd = #passwd# AND isValid = 1
</select>

Jak to działa? Uruchamia się zapytanie nazwane getUser. Do interpretacji wyników używana jest mapa nazwana getUserMap. Zgodnie z tą mapą tworzony jest obiekt klasy pckg.User i wywoływana jest jego metoda setId z wartością pochodzącą z kolumny userId wyniku zapytania. Następnie tworzony jest i analogicznie przypisywany obiekt odpowiadający typowi zmiennej rolesSet, a więc klasy RolesSet. Uwaga! Data Mapper instancjonuje nasze klasy, muszą mieć one zatem bezargumentowy, publiczny konstruktor. Teraz trzeba by coś przypisać do zmiennej rolesSet.roles. To, co zostanie do niej przypisane determinuje zapytanie getRoles uruchamiane z parametrem wejściowym pochodzącym z kolumny userId wyniku poprzedniego zapytania. Do interpretacji wyniku zapytania getRoles służy mapa getRolesMap. Wynikiem są więc obiekty typu java.lang.String, string to predefiniowany alias. Zmiennej rolesSet.roles zostanie przypisany zbiór zawierający te właśnie obiekty. I to koniec, ale wcale nie musimy zatrzymywać się w tym miejscu. Zamiast obiektów typu napisowego moglibyśmy tam mieć dowolne inne i ciągnąć analogiczny łańcuch wywołań dalej. Uwaga! Czegoś takiego nie da się zrobić używając języka zapytań dla Java Persistence API, JPQL’a! Tam konstrukcji JOIN FETCH można użyć tylko raz! Ale to jest inny, całkiem obszerny temat, może na następny artykuł. Tak więc, skonstruowanie naszego obiektu klasy User wymaga dwu zapytań, ale wyobraźmy sobie, że nie chodzi o logowanie do systemu i że pierwsze zapytanie zwraca wielu użytkowników. W ogólności, nie mamy dwu zapytań tylko N + 1, gdzie N to liczba wyników zwracanych przez pierwsze zapytanie. Byłoby słabo, gdyby nie to, że da się to zawsze zrobić pojedynczym zapytaniem, o czym napisałem już we wstępie tego artykułu. Zmieńmy nasz plik mapowania na poniższy:

<resultMap class="pckg.User" id="getUserMap" groupBy="id">
<result property="id" column="userId"/>
<result property="rolesSet.roles" resultMap="namespace.rolesMap"/>
</resultMap>

<resultMap class="string" id="rolesMap">
<result property="value" column="name"/>
</resultMap>

<select id="getUser" parameterClass="pckg.Credentials" resultMap="getUserMap">
SELECT c.userId, r.name
FROM credentials c
LEFT JOIN userRoles u ON c.userId = u.userId AND u.isValid = 1
LEFT JOIN role r ON u.roleId = r.id
WHERE c.login = #login# AND c.passwd = #passwd# AND c.isValid = 1
</select>

Mamy teraz jedno zapytanie, które łączy wszystkie trzy tabele. Wynik tego zapytania to odpowiedni podzbiór iloczynu kartezjańskiego tych tabel. Zauważmy, że będzie tam tyle wierszy ile ról ma przypisanych nasz obiekt User (powracamy do założenia, że jeden login i hasło odpowiada jednemu użytkownikowi). A teraz zwróćmy uwagę na artybut @groupBy mapy getUserMap. Spowoduje on, że w rezultacie otrzymamy tylko tyle obiektów klasy User ile różnych wartości userId zwróci zapytanie getUser (kolumna userId jest mapowana na właściwość id), a więc jeden obiekt. Przyporządkowanie zmiennej rolesSet.roles mapy rolesMap spowoduje odpowiednie zainicjalizowanie zbioru ról. I tu znowu porównam do Java Persistence API. Analogiczna konstrukcja w JPQL, z wykorzystaniem mechanizmu JOIN FETCH ma tą zaskakującą cechę, że w wyniku, zamiast jednego obiektu User otrzymujemy ich tyle ile wierszy zwróciło wygenerowane zapytanie SQL, podobne do powyższego. Ale jak już wspominałem, JOIN FETCH zasługuje na swój osobny artykuł.

Brak komentarzy: