Mapowanie encji w JPA - Strategia złączeniowa
Z Jacek Laskowski - Wiki Projektanta Java EE
Dobrym miernikiem nabytej wiedzy o Korporacyjnej Javie 5 (Java EE 5) może być podejście do egzaminów SCBCD, SCWCD czy SCEA , ale również aktywne uczestnictwo w grupach dyskusyjnych poruszających tą tematykę. Jedną z takich grup jest grupa dyskusyjna użytkowników serwera aplikacyjnego Java EE 5 - Apache Geronimo - user@geronimo.apache.org. Ostatnie pytanie error writing tuple to database "the owning entity is not mapped" geronimo 2.1.1 zdopingowało mnie do dokładniejszego przyjrzenia się tematowi odwzorowywania hierarchii dziedziczenia obiektowego klas encji na struktury relacyjne w bazie danych. W zasadzie teoretycznie temat został przedstawiony w mojej relacji z lektury specyfikacji Java Persistence API (JPA) - Java Persistence - Rozdział 2 Entities zakończony, ale pewnie z braku przykładów czułem pewien niedosyt.
Specyfikacja JPA udostępnia 3 strategie odwzorowywania (mapowania) hierarchii dziedziczenia encji do bazy relacyjnej w JPA - dedykowana tabela dla hierarchii klasy (ang. Single Table per Class Hierarchy Strategy), dedykowana tabela dla każdej klasy encji (ang. Table per Concrete Class Strategy) oraz połączone tabele dla konkretnej encji (ang. Joined Subclass Strategy) (teraz wydaje mi się, że owe tłumaczenia są jakieś od czapy i nie odpowiadają ich angielskim odpowiednikom). Zajmę się tą ostatnią, od tej pory nazywaną strategią złączeniową, gdzie wspólne atrybuty trwałe encji są mapowane do pojedyńczej tabeli, podczas gdy elementy specyficzne dla klasy pochodnej (podklasy) są zapisywane do jej własnej tabeli. Oczywiście wiązanie między tabelami realizowane jest za pomocą kluczy obcych w tabeli atrybutów specyficznych dla klasy pochodnej (tabeli klasy pochodnej) wskazujących na klucz główny tabeli klasy nadrzędnej (tabeli atrybutów wspólnych dla hierarchii klas pochodnych). Strategia złączeniowa odzwierciedla postrzeganie obiektowe encji w bazie relacyjnej, jednakże posiada tą wadę, że do pobrania danych z bazy danych (zmaterializowanie encji) potrzebne są zapytania korzystające z kosztownych czasowo złączeń JOIN, co przy rozległej hierarchii dziedziczenia encji może niekorzystnie odbić się na wydajności zapytań, a co za tym idzie i samej aplikacji. Pora na przykład (podpieram się artykułem Testowanie złączeń FETCH JOIN w JPA).
Kompletny projekt jpa-joined do uruchomienia dostępny jest do pobrania jako jpa-joined.zip.
Spis treści |
Utworzenie projektu - jpa-joins
Korzystam z Apache Maven 2 do utworzenia projektu poleceniem mvn archetype:create.
jlaskowski@work /cygdrive/c/projs/sandbox $ mvn archetype:create -DgroupId=pl.jaceklaskowski.jpa -DartifactId=jpa-joined -Dversion=1.0 [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'archetype'. ... [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating OldArchetype: maven-archetype-quickstart:RELEASE [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: pl.jaceklaskowski.jpa [INFO] Parameter: packageName, Value: pl.jaceklaskowski.jpa [INFO] Parameter: basedir, Value: c:\projs\sandbox [INFO] Parameter: package, Value: pl.jaceklaskowski.jpa [INFO] Parameter: version, Value: 1.0 [INFO] Parameter: artifactId, Value: jpa-joined [INFO] ********************* End of debug info from resources from generated POM *********************** [INFO] OldArchetype created in dir: c:\projs\sandbox\jpa-joined [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------
Projekt znajduje się w katalogu jpa-joined.
Wprowadzenie zależności projektowych - pom.xml
Korzystając z Eclipse IDE 3.4 (Ganymede) i wtyczki m2eclipse importuję projekt, a następnie zmieniam pom.xml projektu na następujący:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.jaceklaskowski.jpa</groupId>
<artifactId>jpa-joined</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<name>Mapowanie encji w JPA - Strategia złączeniowa</name>
<dependencies>
<dependency>
<groupId>org.apache.openjpa</groupId>
<artifactId>openjpa</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.4.1.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Deklaruję w nim użycie Apache OpenJPA 1.1.0 jako dostawcy JPA oraz bazy danych Apache Derby 10.4.1.3. Poza tym określam wersję JUnit 4.4 oraz poziom kompilacji klas w projekcie na poziomie Java 5.0 (wtyczka maven-compiler-plugin oznacza ją jako 1.5).
Encja JPA - Zwierze
Czas na stworzenie pierwszej z dwóch encji w aplikacji - Zwierze - reprezentowanej przez klasę pl.jaceklaskowski.jpa.Zwierze.
package pl.jaceklaskowski.jpa;
import static javax.persistence.InheritanceType.JOINED;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
@Entity
@Inheritance(strategy = JOINED)
public abstract class Zwierze {
@Id
@GeneratedValue
private int id;
private String nazwa;
public Zwierze() {
}
public Zwierze(String nazwa) {
this.nazwa = nazwa;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNazwa() {
return nazwa;
}
public void setNazwa(String nazwa) {
this.nazwa = nazwa;
}
}
Najważniejszą cechą klasy jest adnotacja @Inheritance(strategy = JOINED), która definiuje strategię mapowania klas do odpowiadających im tabel w bazie relacyjnej na strategię złączeniową. Dodatkowo warto zauważyć, że klasa Zwierze jest klasą abstrakcyjną, gdyż zakładam, że nie istnieje sytuacja, w której bezpośrednio można stworzyć egzemplarz tej klasy.
Plik Zwierze.java umieszczam w katalogu src/main/java/pl/jaceklaskowski/jpa.
Encja JPA - Pies
Kolejną z encji jest Pies reprezentowana przez klasę pl.jaceklaskowski.jpa.Pies. Jest to typowa klasa encyjna zawierająca specyficzne dla modelowanego bytu atrybuty.
package pl.jaceklaskowski.jpa;
import javax.persistence.Entity;
@Entity
public class Pies extends Zwierze {
// TODO: Typ wyliczeniowy czy dedykowana klasa?
private String kolorSiersci;
public Pies() {
}
public Pies(String nazwa, String kolorSiersci) {
super(nazwa);
this.kolorSiersci = kolorSiersci;
}
public String getKolorSiersci() {
return kolorSiersci;
}
public void setKolorSiersci(String kolorSiersci) {
this.kolorSiersci = kolorSiersci;
}
}
Plik Pies.java umieszczam w katalogu src/main/java/pl/jaceklaskowski/jpa.
Konfiguracja JPA - persistence.xml
Sercem każdej aplikacji JPA jest plik konfiguracyjny - persistence.xml, w którym wskazuje się m.in. parametry połączenia z bazą danych.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="derbyPU" transaction-type="RESOURCE_LOCAL">
<provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
<class>pl.jaceklaskowski.jpa.Zwierze</class>
<class>pl.jaceklaskowski.jpa.Pies</class>
<properties>
<property name="openjpa.ConnectionDriverName" value="org.apache.derby.jdbc.EmbeddedDriver" />
<property name="openjpa.ConnectionURL" value="jdbc:derby:target/derbyDB;create=true" />
<property name="openjpa.ConnectionUserName" value="app" />
<property name="openjpa.ConnectionPassword" value="app" />
<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(SchemaAction='add,deleteTableContents')" />
<property name="openjpa.Log" value="DefaultLevel=WARN,SQL=TRACE" />
</properties>
</persistence-unit>
</persistence>
Plik persistence.xml umieszczamy w katalogu src/main/resources/META-INF.
Klasa testowa - JoinedTest
W celach demonstracyjnych tworzę klasę testową pl.jaceklaskowski.jpa.JoinedTest.
package pl.jaceklaskowski.jpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class JoinedTest {
private static final String IMIE_OWCZARKA = "Ugryź";
EntityManagerFactory emf;
EntityManager em;
int owczarekId;
@Before
public void setUp() throws Exception {
emf = Persistence.createEntityManagerFactory("derbyPU");
em = emf.createEntityManager();
Pies owczarek = new Pies(IMIE_OWCZARKA, "podpalany");
// Zapisuję encję do bazy danych
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(owczarek);
tx.commit();
// Zapisz identyfikator owczarka "na boku"
owczarekId = owczarek.getId();
// Wyczyść kontekst trwały
em.clear();
}
@Test
public void zaprezentujStrategieZlaczeniowa() {
Pies owczarek = em.find(Pies.class, owczarekId);
assert owczarek.getNazwa().equals(IMIE_OWCZARKA);
}
@After
public void tearDown() throws Exception {
em.close();
emf.close();
}
}
Plik JoinedTest.java umieszczam w katalogu src/test/java/pl/jaceklaskowski/jpa.
Uruchomienie
Uruchomienie klasy testowej zlecam mavenowi za pomocą polecenia mvn -Dtest=JoinedTest clean test.
jlaskowski@work /cygdrive/c/projs/sandbox/jpa-joined $ mvn -Dtest=JoinedTest clean test [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Building Mapowanie encji w JPA - Strategia złączeniowa [INFO] task-segment: [clean, test] ... ------------------------------------------------------- T E S T S ------------------------------------------------------- Running pl.jaceklaskowski.jpa.JoinedTest 3578 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 31291190> executing stmnt 31639617 CREATE TABLE OPENJPA_SEQUENCE_TABLE (ID SMALLINT NOT NULL, SEQUENCE_VALUE BIGINT, PRIMARY KEY (ID)) 3641 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 31291190> [63 ms] spent 3641 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 22511833> executing stmnt 22816835 CREATE TABLE Pies (id INTEGER NOT NULL, kolorSiersci VARCHAR(255), PRIMARY KEY (id)) 3656 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 22511833> [15 ms] spent 3656 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 7833509> executing stmnt 24391166 CREATE TABLE Zwierze (id INTEGER NOT NULL, nazwa VARCHAR(255), PRIMARY KEY (id)) 3672 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 7833509> [16 ms] spent 3672 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 13738411> executing stmnt 17972912 DELETE FROM OPENJPA_SEQUENCE_TABLE 3734 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 13738411> [62 ms] spent 3734 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 13738411> executing stmnt 22106538 DELETE FROM Pies 3734 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 13738411> [0 ms] spent 3734 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 13738411> executing stmnt 11117356 DELETE FROM Zwierze 3750 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 13738411> [16 ms] spent 4156 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 10265083> executing prepstmnt 22033496 SELECT SEQUENCE_VALUE FROM OPENJPA_SEQUENCE_TABLE WHERE ID = ? FOR UPDATE WITH RR [params=(int) 0] 4156 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 10265083> [0 ms] spent 4172 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 30940873> executing prepstmnt 23536061 INSERT INTO OPENJPA_SEQUENCE_TABLE (ID, SEQUENCE_VALUE) VALUES (?, ?) [params=(int) 0, (int) 1] 4172 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 30940873> [0 ms] spent 4172 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 21789336> executing prepstmnt 4409470 SELECT SEQUENCE_VALUE FROM OPENJPA_SEQUENCE_TABLE WHERE ID = ? FOR UPDATE WITH RR [params=(int) 0] 4172 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 21789336> [0 ms] spent 4187 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 21789336> executing prepstmnt 11105479 UPDATE OPENJPA_SEQUENCE_TABLE SET SEQUENCE_VALUE = ? WHERE ID = ? AND SEQUENCE_VALUE = ? [params=(long) 51, (int) 0, (long) 1] 4187 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 21789336> [0 ms] spent 4219 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 27525703> executing prepstmnt 20728841 INSERT INTO Pies (id, kolorSiersci) VALUES (?, ?) [params=(int) 1, (String) podpalany] 4219 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 27525703> [0 ms] spent 4219 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 27525703> executing prepstmnt 14772101 INSERT INTO Zwierze (id, nazwa) VALUES (?, ?) [params=(int) 1, (String) Ugryź] 4219 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 27525703> [0 ms] spent 4406 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 3529877> executing prepstmnt 9742914 SELECT t0.id, t1.nazwa, t0.kolorSiersci FROM Pies t0 INNER JOIN Zwierze t1 ON t0.id = t1.id WHERE t0.id = ? [params=(int) 1] 4406 derbyPU TRACE [main] openjpa.jdbc.SQL - <t 29972655, conn 3529877> [0 ms] spent Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.985 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------
Bardzo pouczające jest przeanalizowanie uruchomienia aplikacji, a szczególnie jej części interakcji Apache OpenJPA z bazą danych, co jest przedstawione komunikatami poprzedzonymi derbyPU TRACE. Widać jak na dłoni, co dzieje się pod spodem naszej aplikacji i jakie polecenia SQL (włączenie z parametrami) są wydawane bazie danych.
Analiza struktur bazodanowych
Poprawne uruchomienie aplikacji to jedno, ale przyczynkiem do jej stworzenia była chęć zbadania struktur bazodanowych tworzonych przy strategii złączeniowej przez Apache OpenJPA. Sprawdzam je narzędziami administracyjnymi Derby.
jlaskowski@work /cygdrive/c/projs/sandbox/jpa-joined $ C\:/apps/db-derby/bin/ij.bat wersja ij 10.3 ij> connect 'jdbc:derby:target/derbyDB'; BłąD XJ040: Uruchomienie bazy danych 'target/derbyDB' nie powiodło się. Szczegóły zawiera następny wyjątek. BłąD XSLAN: Baza danych w C:\projs\sandbox\jpa-joined\target\derbyDB ma format niekompatybilny z bieżącą wersją oprogramowania. Baza danych została utworzona lub aktualizowana w wersji 10.4. ij> quit ;
I faktycznie - trudno się z komunikatem nie zgodzić. Używamy wersji 10.4.1.3 (deklaracja w pom.xml), która najwidoczniej niewspółgra z narzędziami z poprzedniej wersji. Uaktualniam lokalną instalację Derby do wersji 10.4.1.3.
Zacznę od sprawdzenia utworzonych tabel.
jlaskowski@work /cygdrive/c/projs/sandbox/jpa-joined $ C\:/apps/db-derby/bin/ij.bat wersja ij 10.4 ij> connect 'jdbc:derby:target/derbyDB'; ij> show tables in app; TABLE_SCHEM |TABLE_NAME |REMARKS ------------------------------------------------------------------------ APP |OPENJPA_SEQUENCE_TABLE | APP |PIES | APP |ZWIERZE | 3 wierszy wybranych
Analiza struktury tabeli ZWIERZE.
ij> describe ZWIERZE; COLUMN_NAME |TYPE_NAME|DEC&|NUM&|COLUM&|COLUMN_DEF|CHAR_OCTE&|IS_NULL& ------------------------------------------------------------------------------ ID |INTEGER |0 |10 |10 |NULL |NULL |NO NAZWA |VARCHAR |NULL|NULL|255 |NULL |510 |YES 2 wierszy wybranych
oraz PIES
ij> describe PIES; COLUMN_NAME |TYPE_NAME|DEC&|NUM&|COLUM&|COLUMN_DEF|CHAR_OCTE&|IS_NULL& ------------------------------------------------------------------------------ ID |INTEGER |0 |10 |10 |NULL |NULL |NO KOLORSIERSCI |VARCHAR |NULL|NULL|255 |NULL |510 |YES 2 wierszy wybranych
i na koniec przechowywane dane.
ij> select * from zwierze; ID |NAZWA -------------------------------------------------------------------------------------------------------------------------------------------- 1 |Ugryź 1 wiersz wybrany ij> select * from pies; ID |KOLORSIERSCI -------------------------------------------------------------------------------------------------------------------------------------------- 1 |podpalany 1 wiersz wybrany ij> quit;
Wszystko zgodnie z oczekiwaniami - dwie tabele, każda zawierająca wyłącznie kolumny odpowiadające atrybutom trwałym deklarowanym przez poszczególne encje. Nie znam sposobu sprawdzenia istnienia kluczy obcych, więc pozostawiam to jako zadanie domowe. Rozwiązanie proszę przesyłać na moją skrzynkę - jacek@laskowski.net.pl, którym chętnie podzielę się z czytelnikami.
