07 czerwca 2007

Java Persistence - 8.3 Adnotacje zapytań oraz elementy deskryptora XML

Na grupie pl.comp.lang.java od czasu do czasu podnoszona jest dyskusja o stosowaniu Hibernate bezpośrednio a JPA (niekoniecznie z Hibernate jako dostawcą JPA). Ostatnia taka dyskusja Pytanko o Java Persistence API, gdzie 0x28and0x4 napisał:

W szczególności w OpenJPA nie mogę znaleźć mechanizmu "16.1.4. Returning multiple entities":
http://www.hibernate.org/hib_docs/v3/reference/en/html_single/#d0e13126

szczególnie mnie zaintrygowała. Odnotowałem sobie ten wątek do zbadania i tak to trwało, aż do dzisiaj, kiedy natrafiłem na bardzo podobne pytanie o adnotację @SqlResultSetMapping i zwracanie wielu encji przy jednym zapytaniu. Zacząłem czytać specyfikację i po chwili okazało się, że właśnie tego potrzebowałem na odpowiedź na wyżej postawione pytanie. Przyjrzyjmy się rozdziałowi 8.3 Adnotacje zapytań.

Na bazie lektury specyfikacji JPA utworzyłem dokument (ściągę) o adnotacjach JPA - Adnotacje Java Persistence.

Adnotacja @NamedQuery służy do określenia mianowanego zapytania JPQL (język zapytań Java Persistence). Adnotacja posiada element name, który przypisuje unikatowy identyfikator, który służy jako argument dla metody EntityManager.createNamedQuery(java.lang.String).

Adnotacja @NamedNativeQuery służy do określenia mianowanego zapytania SQL. Adnotacja posiada element name, który przypisuje unikatowy identyfikator, który służy jako argument dla metody EntityManager.createNamedQuery(java.lang.String).

Warto zauważyć, że metoda EntityManager.createNamedQuery(java.lang.String) przeznaczona jest do uruchomienia mianowanego zapytania JPQL oraz SQL.

Adnotacje @NamedQuery i @NamedQueries oraz @NamedNativeQuery i @NamedNativeQueries mogą być używane przy klasach encji lub zmapowanych nadklas.

Poza samą konstrukcją zapytania należy pamiętać, że nazwa zapytania (wartość parametru name) musi być unikatowa w całej jednostce trwałości (PU).

Rozdział 8.3.3 Adnotacje dla mapowania wyniku zapytania SQL specyfikacji JPA przedstawia adnotację @SqlResultSetMapping. Adnotacja @SqlResultSetMapping definiuje mapowanie wyniku zapytania SQL.

W rozdziale 10 Deskryptor XML znajduje się informacja, której potrzebowałem do odpowiedzi. W przypadku zapytania SQL, które zwraca wiele encji (rozłącznych w naszym przypadku) dobrym podejściem wydaje się być zastosowanie dekryptora XML - orm.xml do definicji globalnego zapytania (zamiast specyficznego dla danej encji).

Element sql-result-set-mapping jest opisany w specyfikacji JPA w sekcji 10.1.2.9 sql-result-set-mapping. Element sql-result-set-mapping jest globalny dla całej jednostki trwałości i nie ogranicza się wyłącznie dla pojedyńczej encji. Specyfikacja nie określa zachowania dostawcy JPA w przypadku wystąpienia kilku sql-result-set-mapping o tej samej nazwie w pojedyńczej jednostce trwałości.

Definicja sql-result-set-mapping rozszerza zbiór mapowań pobranych z adnotacji. Jeśli zdefiniowano mapowanie poprzez mechanizm adnotacji, to sql-result-set-mapping z deskryptora XML nadpisuje (przesłania) definicję.

Wracamy do rozdziału 3.6.6 Zapytania SQL, gdzie możemy rozszerzyć wiedzę o zapytaniach SQL. Najważniejsze jest, aby pamiętać, że encje zwrócone jako wynik zapytania mogą być różnego typu. Jeśli wiele encji jest zwracanych przez zapytanie, encje muszą być wymienione i zmapowane do wartości kolumn w zapytaniu poprzez adnotację @SqlResultSetMapping. Mapowanie jest wykorzystywane przez dostawcę JPA do odwzorowania wyników JDBC na odpowiadające im obiekty.

Stworzyłem przykład, który zobrazował mi treść specyfikacji. Utworzyłem dwie encje - DanePodstawowe oraz DaneRozszerzone, które reprezentowały informacje o pracowniku. Definicja tabeli, na którą mapowane były encje wygląda następująco:

CREATE TABLE PRACOWNICY (
NUMER integer PRIMARY KEY DEFAULT nextval('serial'),
IMIE varchar(20) NOT NULL,
NAZWISKO varchar(50) NOT NULL,
STANOWISKO varchar(50),
PENSJA NUMERIC(20, 2),
);

Pierwsza encja DanePodstawowe prezentuje się zwyczajnie:

package pl.jaceklaskowski.jpa.entity;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class DanePodstawowe implements Serializable {

private static final long serialVersionUID = 1L;

private int numer;
private String imie;
private String nazwisko;

@Id
public int getNumer() {
return numer;
}

public void setNumer(int numer) {
this.numer = numer;
}

public String getImie() {
return imie;
}

public void setImie(String imie) {
this.imie = imie;
}

public String getNazwisko() {
return nazwisko;
}

public void setNazwisko(String nazwisko) {
this.nazwisko = nazwisko;
}

}

Kolejna encja DaneRozszerzone wygląda tak:

package pl.jaceklaskowski.jpa.entity;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class DaneRozszerzone implements Serializable {
private static final long serialVersionUID = 1L;

private int numer;
private String stanowisko;
private double pensja;

@Id
public int getNumer() {
return numer;
}

public void setNumer(int numer) {
this.numer = numer;
}

public String getStanowisko() {
return stanowisko;
}

public void setStanowisko(String stanowisko) {
this.stanowisko = stanowisko;
}

public double getPensja() {
return pensja;
}

public void setPensja(double pensja) {
this.pensja = pensja;
}
}

Do encji dołączyłem deskryptor XML - orm.xml:

<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" version="1.0">
<named-native-query name="pobierzDanePracownika" result-set-mapping="mapowanieDanychPracownikaPerTyp">
<query>SELECT numer AS n1, numer AS n2 FROM Pracownicy</query>
</named-native-query>
<sql-result-set-mapping name="mapowanieDanychPracownikaPerTyp">
<entity-result entity-class="pl.jaceklaskowski.jpa.entity.DanePodstawowe">
<field-result name="numer" column="n1"/>
</entity-result>
<entity-result entity-class="pl.jaceklaskowski.jpa.entity.DaneRozszerzone">
<field-result name="numer" column="n2"/>
</entity-result>
</sql-result-set-mapping>
</entity-mappings>

Na zakończenie sama klasa testu, która weryfikuje poprawność konfiguracji - SqlResultSetMappingTest.

package pl.jaceklaskowski.jpa.sqlresultsetmapping;

import java.util.List;

import javax.persistence.Query;

import org.testng.annotations.Test;

import pl.jaceklaskowski.jpa.BaseTest;
import pl.jaceklaskowski.jpa.entity.DanePodstawowe;
import pl.jaceklaskowski.jpa.entity.DaneRozszerzone;

public class SqlResultSetMappingTest extends BaseTest {
@Test(dependsOnMethods = { "utworzPracownikow" })
public void testNamedQuery() {
Query query = em.createNamedQuery("pobierzDanePracownika");
List dane = (List) query.getResultList();
assert dane.size() > 0;
final Object[] pierwszyWiersz = dane.get(0);
final Class clazz1 = pierwszyWiersz[0].getClass();
final Class clazz2 = pierwszyWiersz[1].getClass();
assert clazz1 == DanePodstawowe.class : clazz1;
assert clazz2 == DaneRozszerzone.class : clazz2;
}
}

Wykonanie testu nie pozostawia złudzeń, że wszystko jest w należytym porządku i temat został zrozumiany. Pozostaje jedynie nauczyć się adnotacji i elementów desktyptora XML na egzamin SCBCD 5 ;-)

jlaskowski@dev /cygdrive/c/projs/jpa
$ mvn -Popenjpa-postgres -Dtest=SqlResultSetMappingTest clean test
[INFO] Scanning for projects...
[INFO] ----------------------------------------------------------------------------
[INFO] Building jpa-query
[INFO] task-segment: [clean, test]
[INFO] ----------------------------------------------------------------------------
...
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running pl.jaceklaskowski.jpa.sqlresultsetmapping.SqlResultSetMappingTest
...
47 testPU INFO [main] openjpa.Runtime - Starting OpenJPA 1.0.0-SNAPSHOT
...
databaseProductName: PostgreSQL
databaseProductVersion: 8.2.3
driverName: PostgreSQL Native Driver
driverVersion: PostgreSQL 8.2 JDBC3 with SSL (build 504)
...
1313 testPU TRACE [main] openjpa.Query - Executing query: SELECT numer AS n1, numer AS n2 FROM Pracownicy
1344 testPU TRACE [main] openjpa.jdbc.SQL - executing prepstmnt 25567987 SELECT numer AS n1, numer AS n2 FROM Pracownicy
1344 testPU TRACE [main] openjpa.jdbc.SQL - [0 ms] spent
1359 testPU TRACE [main] openjpa.jdbc.JDBC - [0 ms] close
1375 testPU TRACE [main] openjpa.MetaData - Clearing metadata repository "org.apache.openjpa.jdbc.meta.MappingRepository@3ac93e".
1375 testPU TRACE [main] openjpa.MetaData - Clearing cache of parsed files in "org.apache.openjpa.persistence.jdbc.XMLPersistenceMappingParser@1722456".
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.703 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6 seconds
[INFO] Finished at: Thu Jun 07 15:45:36 CEST 2007
[INFO] Final Memory: 11M/254M
[INFO] ------------------------------------------------------------------------