16 lipca 2007

Java Persistence - Rozdział 5.9 Zasady współpracy kontenera i dostawcy JPA

Rozdział 5.9 Zasady współpracy kontenera i dostawcy JPA (ang. Runtime Contracts between the Container and Persistence Provider) specyfikacji Java Persistence 1.0 opisuje zasady współpracy kontenera z dostawcą JPA w celu udostępnienia ujednoliconego interfejsu programistycznego do integracji dostawców JPA firm trzecich (innych niż dostawca domyślny JPA w wybranym kontenerze/serwerze). Specyfikacja podkreśla, że jeśli kontener nie wspiera integracji dostawców JPA innych niż domyślny dostawca JPA, kontener może korzystać z innego zestawu metod.

5.9.1 Wymagania stawiane kontenerowi

Do zarządzania kontekstów trwałych pojedyńczej transakcji, jeśli nie istnieje zarządca encji związany z aktywną transakcją JTA, wtedy:
  • Kontener tworzy nowego zarządcę encji wywołując metodę EntityManagerFactory.createEntityManager() podczas pierwszego wywołania zarządcy typu PersistenceContextType.TRANSACTION w ramach metody biznesowej wykonywanej w transakcji JTA.
  • Po zakończeniu transakcji JTA (zatwierdzenie/wycofanie), kontener zamyka zarządcę encji wywołując metodę EntityManager.close() (specyfikacja zauważa, że kontener ma prawo zarządzać zarządcami encji przez pulę zarządców, więc zamiast faktycznego wywołania metody close() może jedynie nastąpić wyczyszczenie go przez wywołanie EntityManager.clear()).
Kontener musi zgłosić wyjątek TransactionRequiredException, jeśli korzystając z kontekstu trwałego pojedyńczej transakcji metody typu EntityManager: persist, remove, merge oraz refresh są wywołane bez aktywnej transakcji.

Przykładowe ziarno bezstanowe EJB - EJB3TestServiceBean:

@Stateless
public class EJB3TestServiceBean implements EJB3TestService {

@PersistenceContext(unitName = "TestyEJB3EncjePU")
private EntityManager em;

@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public void uruchom() {
Prelegent jacek = new Prelegent("Jacek", "Laskowski");
try {
em.persist(jacek);
} catch (TransactionRequiredException oczekiwano) {
System.out.println(">>> Oczekiwano wyjątku i faktycznie jest " + oczekiwano);
oczekiwano.printStackTrace();
}
}
}

uruchomione na Glassfish v2 b54 kończy się wspomnianym wyjątkiem - TransactionRequiredException.

[#|2007-07-16T14:13:29.437+0200|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=22;_ThreadName=p: thread-pool-1; w: 8;|
--- Oczekiwano wyjątku i faktycznie jest javax.persistence.TransactionRequiredException|#]

[#|2007-07-16T14:13:29.437+0200|WARNING|sun-appserver9.1|javax.enterprise.system.stream.err|_ThreadID=22;_ThreadName=p: thread-pool-1; w: 8;_RequestID=09ef8733-9d34-4675-a15c-c6e546238a91;|
javax.persistence.TransactionRequiredException
at com.sun.enterprise.util.EntityManagerWrapper.doTxRequiredCheck(EntityManagerWrapper.java:236)
at com.sun.enterprise.util.EntityManagerWrapper.doTransactionScopedTxCheck(EntityManagerWrapper.java:200)
at com.sun.enterprise.util.EntityManagerWrapper.persist(EntityManagerWrapper.java:426)
at pl.jaceklaskowski.ejb3.testy.transactions.EJB3TestServiceBean.uruchom(EJB3TestServiceBean.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at com.sun.enterprise.security.application.EJBSecurityManager.runMethod(EJBSecurityManager.java:1067)
at com.sun.enterprise.security.SecurityUtil.invoke(SecurityUtil.java:176)
at com.sun.ejb.containers.BaseContainer.invokeTargetBeanMethod(BaseContainer.java:2884)
at com.sun.ejb.containers.BaseContainer.intercept(BaseContainer.java:3975)
at com.sun.ejb.containers.EJBObjectInvocationHandler.invoke(EJBObjectInvocationHandler.java:203)
at com.sun.ejb.containers.EJBObjectInvocationHandlerDelegate.invoke(EJBObjectInvocationHandlerDelegate.java:117)
at $Proxy53.uruchom(Unknown Source)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at com.sun.corba.ee.impl.presentation.rmi.ReflectiveTie._invoke(ReflectiveTie.java:125)
at com.sun.corba.ee.impl.protocol.CorbaServerRequestDispatcherImpl.dispatchToServant(CorbaServerRequestDispatcherImpl.java:658)
at com.sun.corba.ee.impl.protocol.CorbaServerRequestDispatcherImpl.dispatch(CorbaServerRequestDispatcherImpl.java:198)
at com.sun.corba.ee.impl.protocol.CorbaMessageMediatorImpl.handleRequestRequest(CorbaMessageMediatorImpl.java:1820)
at com.sun.corba.ee.impl.protocol.CorbaMessageMediatorImpl.handleRequest(CorbaMessageMediatorImpl.java:1680)
at com.sun.corba.ee.impl.protocol.CorbaMessageMediatorImpl.handleInput(CorbaMessageMediatorImpl.java:1062)
at com.sun.corba.ee.impl.protocol.giopmsgheaders.RequestMessage_1_2.callback(RequestMessage_1_2.java:194)
at com.sun.corba.ee.impl.protocol.CorbaMessageMediatorImpl.handleRequest(CorbaMessageMediatorImpl.java:780)
at com.sun.corba.ee.impl.protocol.CorbaMessageMediatorImpl.dispatch(CorbaMessageMediatorImpl.java:537)
at com.sun.corba.ee.impl.protocol.CorbaMessageMediatorImpl.doWork(CorbaMessageMediatorImpl.java:2541)
at com.sun.corba.ee.impl.orbutil.threadpool.ThreadPoolImpl$WorkerThread.run(ThreadPoolImpl.java:524)
|#]

dla następujacego persistence.xml:

<?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="TestyEJB3EncjePU" transaction-type="JTA">
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<jta-data-source>jdbc/TestyEjb3Postgres</jta-data-source>
<class>pl.jaceklaskowski.ejb3.testy.encje.Prelegent</class>
<properties>
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.logging.level" value="INFO" />
</properties>
</persistence-unit>
</persistence>

W przypadku stanowych ziaren sesyjnych EJB z rozszerzonym kontekstem trwałym wymagane jest od kontenera, aby:
  • Kontener tworzy zarządcę encji przez EntityManagerFactory.createEntityManager() podczas tworzenia stanowego ziarna EJB, które zadeklarowało zależność od zarządcy encji typu PersistenceContextType.EXTENDED.
  • Kontener zamyka zarządcę wywołując EntityManager.close() po usunięciu stanowego ziarna EJB oraz wszystkich innych stanowych ziaren EJB, które odziedziczyły (=którym przekazano) ten sam kontekst trwały.
  • Podczas wywołania metody biznesowej stanowego ziarna sesyjnego EJB, jeśli stanowe ziarno EJB korzysta z określania zasięgu transakcji przez kontener (ang. CMTD - container managed transaction demarcation), a zarządca encji nie jest związany z bieżącą transakcją JTA, kontener związuje zarządcę z bieżącą transakcją JTA wywołując metodę EntityManager.joinTransaction(). Jeśli istnieje inny kontekst trwały już związany z bieżącą transakcją JTA, kontener zgłasza wyjątek EJBException.

    Niestety nie jestem w stanie przygotować testu, który obrazowałby tą sytuację. Jeśli są chętni pomóc mi zamodelować ten warunek i uzyskać EJBException zapraszam do kontaktu. Popytam w międzyczasie na grupach. Może w ogóle nie da się tego odtworzyć po stronie klienckiej?

  • Podczas wywołania metody biznesowej stanowego ziarna sesyjnego EJB, jeśli stanowe ziarno EJB korzysta z określania zasięgu transakcji przez komponent (ang. BMTD - bean managed transaction demarcation) i UserTransaction jest aktywny, kontener związuje kontekst trwały z transakcją JTA i wywołuje EntityManager.joinTransaction().

Kontener musi zgłasić wyjątek IllegalStateException, jeśli aplikacja wywołuje EntityManager.close() na zarządcy encji zarządzanym przez kontener.

Caused by: java.lang.IllegalStateException
at com.sun.enterprise.util.EntityManagerWrapper.close(EntityManagerWrapper.java:1056)
at pl.jaceklaskowski.ejb3.testy.przekazywaniekontekstu.ZamykajacyBean.wykonaj(ZamykajacyBean.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at com.sun.enterprise.security.application.EJBSecurityManager.runMethod(EJBSecurityManager.java:1067)
at com.sun.enterprise.security.SecurityUtil.invoke(SecurityUtil.java:176)
at com.sun.ejb.containers.BaseContainer.invokeTargetBeanMethod(BaseContainer.java:2884)
at com.sun.ejb.containers.BaseContainer.intercept(BaseContainer.java:3975)
at com.sun.ejb.containers.EJBObjectInvocationHandler.invoke(EJBObjectInvocationHandler.java:203)
... 17 more

dla wywołania EntityManager.close() w następującym bezstanowym ziarnie EJB:

@Stateless
public class ZamykajacyBean implements OperatorKontekstuTrwalego {

@PersistenceContext
EntityManager em;

public void wykonaj() {
em.close(); // niedozwolone
}
}

Kiedy kontener tworzy zarządcę encji, przy pomocy EntityManagerFactory.createEntityManager(Map map) może przekazać mapę właściwości dla dostawcy trwałego. Jeśli właściwości zostały zdefiniowane poprzez @PersistenceContext lub element persistence-context-ref w deskryptorze XML poprzez wywołanie metody EntityManagerFactory.createEntityManager(Map map) zostaną one przekazane dostawcy.

5.9.2 Wymagania stawiane dostawcy JPA

Dostawca JPA nie rozróżnia podziału na konteksty trwałe pojedyńczej transakcji i rozszerzone. Rolą dostawcy JPA jest dostarczanie zarządców encji na żądanie kontenera i zarejestrowanie siebie do otrzymywania zdarzeń związanych z transakcjami (w ten sposób to rolą kontenera jest odpowiednie wywoływanie metod rozpoczynających i kończących transakcję, co przekłada się na wysyłanie zmian stanu transakcji do dostawcy JPA. Rozróżnienie pojedyńczej i rozszerzonej transakcji widoczne jest na poziomie kontenera).

Wywołanie EntityManagerFactory.createEntityManager() przez kontener powoduje utworzenie i zwrócenie nowego zarządcy encji przez dostawcę. Jeśli istnieje aktywna transakcja JTA, dostawca musi zarejestrować się w celu otrzymywania zmian stanu bieżącej transakcji JTA.

Podczas wywołania EntityManager.joinTransaction() dostawca rejestruje się w celu otrzymywania zgłoszeń związanych z bieżącą transakcją JTA jeśli wcześniejsze wywołanie EntityManager.joinTransaction() nie zostało jeszcze zrealizowane (np. w przypadku braku aktywnej transakcji).

Podczas zatwierdzania transakcji JTA, dostawca musi wysłać wszystkie zmiany encji do bazy danych.

Podczas wycofywania transakcji JTA, dostawca musi odłączyć wszystkie zarządzane encje.

Kiedy dostawca zgłasza wyjątek, który zdefiniowano jako wycofujący transakcję, dostawca zaznacza bieżącą transakcję do wycofania.

Podczas wywołania EntityManager.close() dostawca powinien zwolnić wszystkie zasoby, które zostały w międzyczasie utworzone. Pełne zwolnienie zasobów nastąpi dopiero podczas zakończenia wszystkich aktywnych transakcji, w których zarządca encji i zarządzane przez niego zasoby brały udział. Jeśli zarządca jest już zamknięty, dostawca zgłasza wyjątek IllegalStateException.

Podczas wywołania EntityManager.clear(), dostawca musi wyczyścić kontekst trwały, a tym samym odłączyć wszystkie zarządzane encje (encje przejdą w stan odłączony).

public void testEntityManagerAPI() {
Osoba encja = new Osoba("Jacek", "Laskowski");
EntityTransaction et = em.getTransaction();
et.begin();
em.persist(encja);
et.commit();
assert em.contains(encja) : "Encja musi być w kontekście trwałym (musi być zarządzana)";
em.clear();
assert !em.contains(encja) : "Encja nie może być w kontekście trwałym (musi być odłączona)";
assert encja.getNumer() > 0 : "Encja musi mieć przypisany identyfikator trwały chociaż nie jest zarządzana";
}

Tym samym lekturę rozdziału 5. o zarządcach encji i kontekstach trwałych uważam za zakończoną. Poza kilkoma miejscami, które wymagają doprecyzowania przez utworzenie testów, które demonstrowałyby temat w akcji, wszystko zostało powiedziane...ekhm...napisane. Jutro się wszystko okaże podczas prezentacji JPA na spotkaniu Warszawa JUG. Zapraszam na 18:00 na MIMUW na Banacha 2.