02 kwietnia 2007

Optymistyczne blokowanie w JPA - adnotacja @Version i zbiór doświadczeń z dostawcami JPA

Na grupie pl.comp.lang.java pojawiła się dyskusja odnośnie działania adnotacji @Version w Java Persistence (JPA), którą zapoczątkowała Gabcia85 - @Version, problem z optimistic locking w EJB3.0. Po kilku wiadomościach okazało się, że potrzeba nam przykładowej aplikacji, która zademonstruje funkcjonalność adnotacji @Version. Ja wciąż potrzebuję środowiska do analizy działania EJB 3.0, do której przymierzam się zaraz po zakończeniu lektury specyfikacji JPA i łącząc przyjemne z pożytecznym postanowiłem popróbować się z tematem @Version oraz zestawienia środowiska do nauki EJB3 (co jak się później okazało nie było potrzebne w tej konkretnej kwestii). Środowisko do nauki EJB 3.0 jeszcze niegotowe, ale temat @Version już tak. Dziękuję Gabcia85 za cierpliwe namawianie mnie na rozpoznanie tematu @Version!

Rozpocznę od wskazania na jedno ze źródeł moich początkowych analiz - blog grupy Amis EJB 3.0 Persistence - using the @Version annotation for Optimistic Locking - in the GlassFish Reference Implementation by Lucas Jellema. Początkowo myślałem, że ten artykuł to będzie wszystko, czego potrzebuję, ale szybko okazało się, że potrzebuję więcej. Podobnie jak ja, autor wpisu próbował rozpoznać temat @Version i optymistycznego blokowania i zebrał swoje przemyślenia do dalszej analizy przez odwiedzających. Dodając do tego początkową myśl, że będę potrzebował środowiska do nauki EJB 3.0, aby zmagać się z tematem, można sobie wyobrazić, ile niepotrzebnego bagażu przyszło mi taszczyć początkowo (może chociaż na egzaminie SCBCD 5 nie będzie już tak ciężko?!).

Rozpocznijmy temat optymistycznego blokowania od przybliżenia głównego aktora - adnotacji @Version. Dokumentacja javadoc adnotacji @Version opisuje ją jako sposób na oznaczenie pola lub właściwości encji, który przechowuje wartość służącą jako wartość optymistycznej blokady. Wartość pozwala na zapewnienie integralności danych podczas operacji merge oraz kontroli optymistycznej wielodostępności (a dokładniej równoczesnej modyfikacji encji). Dużo mądrych słów, które sprowadzają się do stwierdzenia, że to pole wskazuje na potencjalne modyfikacje od momentu ostatniego odczytania stanu encji do momentu próby przywiązania odłączonej encji do zarządcy trwałości (operacja merge).

Jedynie jeden atrybut (=pole lub właściwość) encji może zostać udekorowany adnotacją @Version. Stosowanie wielu atrybutów z @Version powoduje, że encja staje się nieprzenośna między dostawcami JPA (co później okaże się prawdą przy testach z OpenJPA, TopLink i Hibernate, tj. jedynie OpenJPA będzie przestrzegał restrykcyjnie rekomendacji).

Istotna uwaga wiąże się z mapowaniem atrybutu z tabelą reprezentującą encję. Atrybut wersjonowany musi być związany (zmapowany) z podstawową tabelą encji. Powiązanie atrybutu wersjonowanego z inną tabelą powoduje, że aplikacja staje się nieprzenośna między dostawcami JPA.

Możliwymi typami atrybutów wersjonowanych mogą być: int, Integer, short, Short, long, Long oraz java.sql.Timestamp.

Specyfikacja Java Persistence 1.0 przedstawia adnotację @Version w rozdziale 9.1.17 Adnotacja Version (strona 178), która de facto jest kopią informacji zawartych w dokumentacji Javadoc poza rekomendacją, aby atrybut wersjonowany nie był modyfikowany przez aplikację. Należy również zwrócić uwagę na operacje zbiorcze (ang. bulk operations), które istotnie wpływają na utrzymywanie wartości atrybutu nie zważając na kontrolę optymistycznego blokowania, co nakłada na aplikację obowiązek samodzielnego zarządzania wartością atrybutu wersjonowanego (jest to jedyny akceptowany przez specyfikację przypadek ingerencji programisty w wartość atrybutu wersjonowanego encji).

Przykład zastosowania wersjonowanego atrybutu w klasie encji.

@Version
public int getWersja() {
return wersja;
}

Dla zobrazowania działania wersjonowanego atrybutu przedstawiam test, który wyłącznie przez zastosowanie instrukcji warunkowej if kończy się pomyślnie dla Apache OpenJPA 0.9.7-SNAPSHOT, TopLink Essentials 2.0 BUILD 40 oraz Hibernate EntityManager 3.3.1.

Query query = em.createQuery("SELECT o FROM Osoba o WHERE o.imie = 'Jacek' AND o.nazwisko = 'Laskowski'");
final Osoba osoba = (Osoba) query.getSingleResult();
final Long numerOsoby = osoba.getNumer(); // numer jest kluczem głównym
long wersja = osoba.getWersja(); // wersja jest wersjonowanym atrybutem
{
EntityTransaction tx = em.getTransaction();
tx.begin();
Osoba osobaWersja0 = em.find(Osoba.class, numerOsoby);
assert osobaWersja0.getWersja() == wersja : "Oczekiwano wersji " + wersja + ", a była "
+ osobaWersja0.getWersja();
osobaWersja0.setImie("ZmianaImienia");
em.flush();

// modyfikacja zostaje zapisana w bazie danych, więc następuje podniesienie wersji
wersja++;

assert osobaWersja0.getWersja() == wersja : "Oczekiwano wersji " + wersja + ", a była "
+ osobaWersja0.getWersja();
tx.commit();
assert osobaWersja0.getWersja() == wersja : "Oczekiwano wersji " + wersja + ", a była "
+ osobaWersja0.getWersja();
em.refresh(osobaWersja0);
assert osobaWersja0.getWersja() == wersja : "Oczekiwano wersji " + wersja + ", a była "
+ osobaWersja0.getWersja();
}
{
em.clear(); // osoba stała się odłączoną encją
final String noweImie = "Nowe imię";
osoba.setImie(noweImie);

EntityTransaction tx = em.getTransaction();
tx.begin();
Osoba osobaWersja1 = em.find(Osoba.class, numerOsoby);
osobaWersja1.setImie("I kolejna zmiana imienia");
tx.commit(); // modyfikacja trafia do bazy danych

wersja++;

assert osobaWersja1.getWersja() == wersja : "Oczekiwano wersji " + wersja + ", a była "
+ osobaWersja1.getWersja();
assert osobaWersja1.getWersja() != osoba.getWersja() : "Oczekiwano, że wersje będą różne, a otrzymano wersje "
+ osobaWersja1.getWersja() + " dla osobaWersja1 oraz " + osoba.getWersja() + " dla osoba";

if (em.getClass().getCanonicalName().equals("org.apache.openjpa.persistence.EntityManagerImpl")) {
// TopLink oraz Hibernate nie zaakceptują związania egzemplarza osoba encji Osoba z zarządcą
// trwałości (EM) zgodnie ze specyfikacją
// OpenJPA pozwala na wykonanie dalszych operacji, aż do momentu próby zapisania zmian do bazy
// danych, np. podczas zatwierdzenia transakcji, albo podczas flush
Osoba osobaPoMerge = em.merge(osoba);

assert osobaPoMerge.getImie().equals(osoba.getImie());
assert osobaPoMerge.getImie().equals(noweImie);

em.getTransaction().begin();
try {
em.flush();
assert false : "Oczekiwano wyjątku w związku z naruszeniem optymistycznego blokowania";
} catch (/* OptimisticLock */Exception oczekiwano) {
// mamy do wyboru nadpisać zmiany dla konkretnej encji, albo wycofać transakcję
// (uwaga na inne modyfikacje pozostałych encji w EM)
// możemy również próbować wykryć zmiany porównując egzemplarze encji Osoba
em.getTransaction().rollback();
}
}
}

Jak można zauważyć w przykładzie OpenJPA umożliwia przywiązanie (operacja merge) egzemplarza encji z zarządcą trwałości mimo modyfikacji jej stanu w bazie danych, co wydaje się być niezgodne ze specyfikacją. Dla przypomnienia przytoczę właściwy fragment specyfikacji (Rozdział 9.1.17 Adnotacja Version strona 178):

The Version annotation specifies the version field or property of an entity class that serves as its optimistic lock value. The version is used to ensure integrity when performing the merge operation and for optimistic concurrency control.

Zgłosiłem błąd OPENJPA-197 w bazie zgłoszeń projektu OpenJPA.

Rzucany wyjątek w OpenJPA to:

<0.9.7-incubating-SNAPSHOT fatal store error> org.apache.openjpa.persistence.OptimisticLockException: Optimistic locking errors were detected when flushing to the data store. The
following objects may have been concurrently modified in another transaction: [pl.jaceklaskowski.jpa.entity.Osoba-pl.jaceklaskowski.jpa.entity.Osoba-1]
at org.apache.openjpa.kernel.BrokerImpl.newFlushException(BrokerImpl.java:2110)
at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:1960)
at org.apache.openjpa.kernel.BrokerImpl.flushSafe(BrokerImpl.java:1858)
at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:1629)
at org.apache.openjpa.kernel.DelegatingBroker.flush(DelegatingBroker.java:975)
at org.apache.openjpa.persistence.EntityManagerImpl.flush(EntityManagerImpl.java:486)
at pl.jaceklaskowski.jpa.chapter9.MetadataTest.testVersion(MetadataTest.java:142)

Podczas uruchomienia testu z Hibernate EntityManager 3.3.1 próba związania odłączonej encji z zarządcą trwałości, kiedy nastąpiło uaktualnienie jej stanu w bazie raportowane jest następująco:

javax.persistence.OptimisticLockException
at org.hibernate.ejb.AbstractEntityManagerImpl.wrapStaleStateException(AbstractEntityManagerImpl.java:643)
at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:600)
at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:237)
at pl.jaceklaskowski.jpa.chapter9.MetadataTest.testVersion(MetadataTest.java:141)

TopLink Essentials 2.0 BUILD 40 raportuje podobnie:

javax.persistence.OptimisticLockException: Exception [TOPLINK-5010] (Oracle TopLink Essentials - 2.0 (Build 40 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockEx
ception
Exception Description: The object [2. Nowe imię Laskowski należy do 3 projektów.] cannot be merged because it has changed or been deleted since it was last read. {3}Class> pl.jacek
laskowski.jpa.entity.Osoba
at oracle.toplink.essentials.internal.ejb.cmp3.base.EntityManagerImpl.mergeInternal(EntityManagerImpl.java:222)
at oracle.toplink.essentials.internal.ejb.cmp3.EntityManagerImpl.merge(EntityManagerImpl.java:113)
at pl.jaceklaskowski.jpa.chapter9.MetadataTest.testVersion(MetadataTest.java:136)

Podczas uruchamiania testów ujawniło się kilka ciekawych kwestii, niekoniecznie bezpośrednio związanych z @Version i optymistycznym blokowaniem. Dowiedziałem się (a w zasadzie w końcu doświadczyłem i zapamiętałem) kolejnej, bardzo istotnej informacji o działaniu JPA - dostęp dostawcy JPA do stanu encji. Wyróżnia się dwa tryby dostępu dostawcy JPA do stanu encji:
  • dostęp po polach (ang. field access)
  • dostęp po właściwościach (ang. property access).
Pamiętamy, że konfiguracja JPA odbywa się poprzez deskryptor XML albo adnotacje. W zależności od umiejscowienia adnotacji wskazujemy na rodzaj dostępu do stanu encji. Jeśli dekorujemy pola przypisanie wartości będzie odbywało się bezpośrednio na polach podczas, gdy przy konfiguracji dostępu po właściwościach będzie angażowana metoda modyfikująca (metoda set, ang. setter method). Konfiguracja dostępu w deskryptorze XML odbywa się poprzez element access encji. I tutaj pojawia się ów konkret, który przez dłuższy moment zajął mój czas - specyfikacja umożliwia korzystanie wyłącznie z jednego rodzaju dostępu - po polach lub właściwościach (Rozdział 2.1.1 Pola i właściwości trwałe strona 18). Przy konfiguracji za pomocą deskryptora XML nie będzie to sprawiało problemu (ustawiamy element access na odpowiednią wartość i po wszystkim), ale w przypadku adnotacji raz możemy dekorować pole, a innym razem metodę odczytującą (metoda get, ang. getter). Dekorowanie pól i metod jednocześnie jest niedozwolone.
Innym ważnym wymaganiem jest dekorowanie wyłącznie metod odczytujących. Ważne, aby zapewnić, że podklasa, która jest również encją, nadal korzystała z wybranego wcześniej sposobu dostępu do stanu encji.
Nie długo trwało zanim okazało się, że opis zarządzania stanem encji po polach bądź właściwościach opisany jest w rodziale 2.1.1 specyfikacji, którą dawno, dawno temu relacjonowałem w Java Persistence - Rozdział 2 Entities - rozpoczęcie rozdziału. Pamiętam, że w przeciwieństwie do początków lektury specyfikacji, teraz próbuję zweryfikować zdobytą wiedzę praktycznie i stąd większe zorientowanie w temacie i dłuższa pamięć. Teraz już nie zapomnę! ;-)

Sprawdzając jak reagują dostawcy JPA podczas stosowania obu dostępów jednocześnie, okazało się, że OpenJPA poinformuje o niezgodnościach podczas uruchomienia:

312 derbyPU TRACE [main] openjpa.MetaData - Clearing metadata repository "org.apache.openjpa.meta.MetaDataRepository@14c194d".
Exception in thread "main" <0.9.7-incubating-SNAPSHOT nonfatal user error> org.apache.openjpa.util.UserException: Type "pl.jaceklaskowski.jpa.entity.PracownikSpecjalny" attempts to
use both field and property access. Only one access method is permitted.
at org.apache.openjpa.meta.AbstractMetaDataDefaults.populate(AbstractMetaDataDefaults.java:152)
at org.apache.openjpa.persistence.PersistenceMetaDataDefaults.populate(PersistenceMetaDataDefaults.java:215)
at org.apache.openjpa.meta.MetaDataRepository.addMetaData(MetaDataRepository.java:733)
at org.apache.openjpa.meta.MetaDataRepository.addMetaData(MetaDataRepository.java:719)
at org.apache.openjpa.persistence.AnnotationPersistenceMetaDataParser.getMetaData(AnnotationPersistenceMetaDataParser.java:639)
at org.apache.openjpa.persistence.AnnotationPersistenceMetaDataParser.parseClassAnnotations(AnnotationPersistenceMetaDataParser.java:467)
at org.apache.openjpa.persistence.AnnotationPersistenceMetaDataParser.parse(AnnotationPersistenceMetaDataParser.java:344)
at org.apache.openjpa.persistence.PersistenceMetaDataFactory.load(PersistenceMetaDataFactory.java:213)
at org.apache.openjpa.meta.MetaDataRepository.getMetaDataInternal(MetaDataRepository.java:414)
at org.apache.openjpa.meta.MetaDataRepository.getMetaData(MetaDataRepository.java:272)
at org.apache.openjpa.enhance.PCEnhancer.(PCEnhancer.java:190)
at org.apache.openjpa.enhance.PCEnhancer.run(PCEnhancer.java:3608)
at org.apache.openjpa.enhance.PCEnhancer.run(PCEnhancer.java:3562)
at org.apache.openjpa.enhance.PCEnhancer.main(PCEnhancer.java:3534)

oraz o bezpośrednim dostępie do pola stanu w encji, której dostęp do stanu odbywa się po właściwościach, co według specyfikacji jest niezalecane (wspomniany rozdział 2.1.1 strona 19):

359 derbyPU WARN [main] openjpa.Enhance - Detected the following possible violations of the restrictions placed on property access persistent types:
"pl.jaceklaskowski.jpa.entity.Osoba" uses property access, but its field "projekty" is accessed directly in method "addProjekt" defined in "pl.jaceklaskowski.jpa.entity.Osoba".
"pl.jaceklaskowski.jpa.entity.Osoba" uses property access, but its field "projekty" is accessed directly in method "toString" defined in "pl.jaceklaskowski.jpa.entity.Osoba".
"pl.jaceklaskowski.jpa.entity.Osoba" uses property access, but its field "imie" is accessed directly in method "toString" defined in "pl.jaceklaskowski.jpa.entity.Osoba".
"pl.jaceklaskowski.jpa.entity.Osoba" uses property access, but its field "numer" is accessed directly in method "toString" defined in "pl.jaceklaskowski.jpa.entity.Osoba".
"pl.jaceklaskowski.jpa.entity.Osoba" uses property access, but its field "nazwisko" is accessed directly in method "toString" defined in "pl.jaceklaskowski.jpa.entity.Osoba".

Więcej informacji na ten temat znajduje się w relacji z rozdziału 2.1.1 Pola i właściwości trwałe w Notatniku.

W międzyczasie podczas rozpoznawania @Version i zestawiania środowiska natknąłem się na kolejną ciekawostkę, tym razem związaną z Apache Maven 2 - wtyczką maven-surefire-plugin oraz OpenJPA PCEnhancer. Zestawienie środowiska wymagało zmodyfikowania dotychczasowej konfiguracji projektu - pom.xml. Profil openjpa przedstawia się następująco (w kolejce oczekuje poprawka do moich artykułów dotyczących M2 i OpenJPA).

<profile>
<id>openjpa</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>org.apache.openjpa</groupId>
<artifactId>openjpa-all</artifactId>
<version>0.9.7-incubating-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>process-classes</phase>
<configuration>
<tasks>
<java classname="org.apache.openjpa.enhance.PCEnhancer" classpathref="maven.runtime.classpath"
dir="target/classes" fork="true" failonerror="true" />
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources/openjpa</directory>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>apache-snapshots</id>
<name>Apache Snapshots Repository</name>
<url>http://people.apache.org/maven-snapshot-repository</url>
<layout>default</layout>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</profile>

Zmiana wynikała z dotychczasowego stosowania nieaktualnej wtyczki openjpa-maven-plugin, która korzystała z wersji Apache OpenJPA 0.9.6-SNAPSHOT. Jeśli korzystasz z wtyczki, natychamiast zamień jej wywołanie na odpowiednie wywołanie wtyczki maven-antrun-plugin jak przedstawiono powyżej. Informacja o tym znajduje się również na stronach projektu OpenJPA - EnhancingWithMaven. Zgłosiłem poprawkę dotyczącą skorzystania z atrybutu failonerror="true" zadania java, gdzie niepomyślnie zakończenie zadania, czyli modyfikacji klas kończyło się niezrozumiałym błędem podczas uruchomienia - OPENJPA-195 Add failonerror="true" to the java task example at enhancingwithmaven.html.

Kolejna lekcja polegała na zmuszeniu odmłodzonego środowiska do pracy. Przez długi czas zmagałem się z konfiguracją OpenJPA 0.9.7-SNAPSHOT oraz maven-antrun-plugin i zależnościami, aż z pomocą przyszedł Antoni Jakubiak, który swoje doświadczenia z PostgreSQL opisał w komentarzu do Przepis na środowisko do praktycznego poznawania JPA. Szukając odpowiedzi natrafiłem na...własny Notatnik i komentarz Antoniego i po dodaniu wpisu do konfiguracji OpenJPA w persistence.xml wszystko zaczęło znowu hulać! Dzięki Antoni!

Aktualny persistence.xml prezentuje się następująco (bardzo istotna linia z parametrem openjpa.QueryCompilationCache):

<?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.entity.Osoba</class>
<class>pl.jaceklaskowski.jpa.entity.Projekt</class>
<class>pl.jaceklaskowski.jpa.entity.PracownikSpecjalny</class>
<exclude-unlisted-classes />
<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=TRACE,SQL=TRACE" />
<!-- Breaks maven-surefire-plugin with TestNG -->
<property name="openjpa.QueryCompilationCache" value="false" />
</properties>
</persistence-unit>
</persistence>

Dla zwrócenia uwagi, podkreślam, że podczas pracy z M2 proponuję każdorazowo usuwać zawartość repozytorium lokalnego M2 przed wykonywaniem testów w projekcie, który korzysta z zależności w wersji SNAPSHOT, jeśli budujemy w trybie połączenia do sieci (ang. online). Doświadczyłem tego nie pierwszy raz i za każdym razem powtarzam sobie, że to będzie ostatni. I niestety nigdy nie jest. Ma to szczególne znaczenie, jeśli publikujemy swoje wyniki.

I kolejna ciekawostka JPA - zapewnienie, że każdy atrybut ma parę metod modyfikującą i odczytującą (Rozdział 2.1.1 Pola i właściwości trwałe strona 18 u dołu):

Exception Description: predeploy for PersistenceUnit [derbyPU] failed.
Internal Exception: Exception [TOPLINK-7174] (Oracle TopLink Essentials - 2.0 (Build 40 (03/21/2007))): oracle.toplink.essentials.exceptions.ValidationException
Exception Description: The getter method [public int pl.jaceklaskowski.jpa.entity.Osoba.getWersja()] on entity class [class pl.jaceklaskowski.jpa.entity.Osoba] does not have a corresponding setter method defined.
at oracle.toplink.essentials.internal.ejb.cmp3.EntityManagerSetupImpl.predeploy(EntityManagerSetupImpl.java:615)
at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.callPredeploy(JavaSECMPInitializer.java:146)
at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.initPersistenceUnits(JavaSECMPInitializer.java:226)
at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.initialize(JavaSECMPInitializer.java:242)
at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.initializeFromMain(JavaSECMPInitializer.java:278)
at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.getJavaSECMPInitializer(JavaSECMPInitializer.java:81)
at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:119)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:60)
at pl.jaceklaskowski.jpa.chapter9.MetadataTest.setUp(MetadataTest.java:37)

Początkowo sądziłem, że brak metody modyfikującej to sposób na usunięcie możliwości modyfikacji atrybutu wersjonowanego (!) Kompletnie zlekceważyłem zapis w specyfikacji dotyczący pól i właściwości trwałych, która nie wspomina o widoczności metod. Stworzyłem prywatną metodę modyfikującą (kwalifikator dostępu private) i...działa!

Ciekawe zachowanie wykazało OpenJPA, kiedy usunąłem metodę modyfikującą. Po prostu nie następowało zwiększenie wartości atrybutu wersjonowanego, więc test zaprezentowany wyżej kończył się z błędem:

testVersion(pl.jaceklaskowski.jpa.chapter9.MetadataTest) Time elapsed: 0.281 sec <<< FAILURE!
java.lang.AssertionError: Oczekiwano wersji 1, a była 0
at pl.jaceklaskowski.jpa.chapter9.MetadataTest.testVersion(MetadataTest.java:110)

Okazało się również, że TopLink rozpoczyna wersjonowanie od numeru 1 podczas, gdy OpenJPA od 0 (nie jest to precyzowane przez specyfikację JPA).

Dodatkowo sprawdziłem raportowanie błędu podwójnego atrybutu wersjonowanego.

Apache OpenJPA 0.9.7-SNAPSHOT zgłasza błąd w ten sposób:

[java] Exception in thread "main" <0.9.7-incubating-SNAPSHOT fatal user error> org.apache.openjpa.util.MetaDataException: Type "pl.jaceklaskowski.jpa.entity.Osoba" has multiple version fields: [pl.jaceklaskowski.jpa.entity.Osoba.versionAsTimestamp, pl.jaceklaskowski.jpa.entity.Osoba.wersja]
[java] at org.apache.openjpa.meta.ClassMetaData.getVersionField(ClassMetaData.java:989)
[java] at org.apache.openjpa.enhance.PCEnhancer.addGetVersionMethod(PCEnhancer.java:1275)
[java] at org.apache.openjpa.enhance.PCEnhancer.addPCMethods(PCEnhancer.java:723)
[java] at org.apache.openjpa.enhance.PCEnhancer.run(PCEnhancer.java:326)
[java] at org.apache.openjpa.enhance.PCEnhancer.run(PCEnhancer.java:3613)
[java] at org.apache.openjpa.enhance.PCEnhancer.run(PCEnhancer.java:3562)
[java] at org.apache.openjpa.enhance.PCEnhancer.main(PCEnhancer.java:3534)

TopLink Essentials 2.0 BUILD 40 zgłosił jedynie ostrzeżenie i zakończył działanie poprawnie:

[TopLink Warning]: 2007.04.01 11:04:23.031--ServerSession(10774273)--Thread(Thread[main,5,main])--An optimistic locking policy is already defined on the descriptor for the entity [
class pl.jaceklaskowski.jpa.entity.Osoba]. Ignoring version specification on element [public java.sql.Timestamp pl.jaceklaskowski.jpa.entity.Osoba.getVersionAsTimestamp()].

a Hibernate EntityManager 3.3.1, podobnie jak TopLink Essentials, wykonał testy poprawnie, jednak bez zgłoszenia niezgodności. Zauważyłem jednak, że Hibernate mimo braku zgłoszenia błędu o wielu atrybutach wersjonowanych, korzystał i tak wyłącznie z jednej - pierwszej, ignorując drugą (!)

22:54:00,765 DEBUG AnnotationBinder:1086 - Processing annotations of pl.jaceklaskowski.jpa.entity.Osoba.versionAsTimestamp
22:54:00,765 DEBUG Ejb3Column:161 - Binding column versionAsTimestamp unique false
22:54:00,765 DEBUG AnnotationBinder:1263 - versionAsTimestamp is a version property
22:54:00,765 DEBUG PropertyBinder:106 - binding property versionAsTimestamp with lazy=false
22:54:00,765 DEBUG SimpleValueBinder:220 - building SimpleValue for versionAsTimestamp
22:54:00,765 DEBUG PropertyBinder:128 - Building property versionAsTimestamp
22:54:00,765 DEBUG PropertyBinder:172 - Cascading versionAsTimestamp with null
22:54:00,765 DEBUG AnnotationBinder:1283 - Version name: versionAsTimestamp, unsavedValue: undefined
22:54:00,765 DEBUG AnnotationBinder:1086 - Processing annotations of pl.jaceklaskowski.jpa.entity.Osoba.wersja
22:54:00,765 DEBUG Ejb3Column:161 - Binding column wersja unique false
22:54:00,765 DEBUG AnnotationBinder:1263 - wersja is a version property
22:54:00,765 DEBUG PropertyBinder:106 - binding property wersja with lazy=false
22:54:00,781 DEBUG SimpleValueBinder:220 - building SimpleValue for wersja
22:54:00,781 DEBUG PropertyBinder:128 - Building property wersja
22:54:00,781 DEBUG PropertyBinder:172 - Cascading wersja with null
22:54:00,781 DEBUG AnnotationBinder:1283 - Version name: wersja, unsavedValue: undefined
...
22:54:03,296 DEBUG AbstractEntityPersister:2746 - Update 0: update Osoba set dzienImienin=?, dzienUrodzin=?, imie=?, kraj=?, nazwisko=?, versionAsTimestamp=?, wersja=?, tytul=? wh
ere numer=? and wersja=?
22:54:03,296 DEBUG AbstractEntityPersister:2747 - Delete 0: delete from Osoba where numer=? and wersja=?
22:54:03,312 DEBUG AbstractEntityPersister:2734 - Static SQL for entity: pl.jaceklaskowski.jpa.entity.Osoba
22:54:03,312 DEBUG AbstractEntityPersister:2739 - Version select: select wersja from Osoba where numer =?

Wniosek: Należy uważać korzystając z atrybutów wersjonowanych z różnymi dostawcami JPA.

Jeśli ktoś nie zauważył, korzystam z najnowszej wersji Hibernate EntityManager 3.3.1, który złamał kontrakt z JBoss 4.0.5 i konieczne było szybkie wydanie nowszej wersji (!) Uaktualnienie repozytorium M2 i pom.xml projektów, które z niego korzystały, trwało kilka sekund!

mvn install:install-file -DgroupId=org.hibernate -DartifactId=hibernate-entitymanager \
-Dversion=3.3.1 -Dpackaging=jar -Dfile=c:/apps/hibernate-entitymanager-3.3.1.GA/hibernate-entitymanager.jar

Uruchomienie testów utwierdza o poprawnej instalacji:

2007-04-01 17:26:51 org.hibernate.ejb.Version <clinit>
INFO: Hibernate EntityManager 3.3.1.GA

Przy okazji włączyłem również niższy poziom komunikatów w Hibernate EntityManager na DEBUG, aby móc prześledzić jego uruchomienie. W przeciwnieństwie do OpenJPA oraz TopLink, których konfiguracja poziomu komunikatów jest możliwa w pliku persistence.xml, Hibernate EM (a w zasadzie commons-logging) wymaga, aby umieścić log4j.jar na ścieżce klas (ang. CLASSPATH), tj. w przypadku korzystania z M2 - dodać zależność log4j w pom.xml, i stworzyć konfigurację komunikatów - log4j.properties, gdzie ustawiamy interesujący nas poziom.

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

log4j.rootLogger=debug, stdout

Na zakończenie dodam, tak przy okazji, że pojawiła się najnowsza wersja Apache Maven 2 - 2.0.6, do której pobrania zachęcam. Ja już zainstalowałem i testy były wykonywane z jej pomocą.

Wracam do lektury specyfikacji JPA. Przypominam również o jutrzejszym VIII spotkaniu Warszawa-JUG, na którym zobaczymy Grzegorza Kossakowskiego z Apache Cocoon 2.2. Zapraszam!