Dostęp do EntityManager w metodach zwrotnych encji w JPA
Z Jacek Laskowski - Wiki Projektanta Java EE
Kolejny wątek na grupie pl.comp.lang.java sprowokował mnie do szczegółowej analizy działania Java Persistence API (JPA) ze specyfikacją w ręku. Tym razem Krzysiek (aka Kolszew) zapytał o mechanizm wstrzeliwania zależności w encjach JPA, które są częścią szerszej specyfikacji Enterprise JavaBeans 3.0 (EJB3) - EJB3, JPA, @EJB. Po krótkiej wymianie wiadomości okazało się, że tematem przewodnim wątku jest możliwość wykorzystania javax.persistence.EntityManager w metodzie zwrotnej encji (oznaczonej przez adnotację @PostRemove). Mechanizm wstrzeliwania zależności odpada w tym scenariuszu, gdyż EJB3 nie udostępnia go dla encji JPA. Jak się okazało adnotacje nie są dostępne, ale podstawy, na których są oparte, jak najbardziej, tj. lokalne drzewo JNDI w kontekście wywołania encji.
Spis treści |
Wstęp teoretyczny - lektura specyfikacji JPA
Zanim zaczniemy rozpoznawać temat praktycznie, warto przyjrzeć się jak temat przedstawiono teoretycznie w specyfikacji JPA - JSR 220: Enterprise JavaBeansTM,Version 3.0 - Java Persistence API.
W rozdziale 3.5 Entity Listeners and Callback Methods (str. 58) napisano:
In general, portable applications should not invoke EntityManager or Query operations, access other entity instances, or modify relationships in a lifecycle callback method.
z adnotacją:
The semantics of such operations may be standardized in a future release of this specification.
Zwrot should not nie oznacza nie można, więc czytamy dalej. Natrafiamy na następujące informacje uzupełniające:
When invoked from within a Java EE environment, the callback listeners for an entity share the enterprise naming context of the invoking component, and the entity callback methods are invoked in the transaction and security contexts of the calling component at the time at which the callback method is invoked.
co oznacza, że skoro:
Lifecycle callbacks can invoke JNDI, JDBC, JMS, and enterprise beans.
więc *teoretycznie* można odszukać zarządcę encji w drzewie JNDI, który właśnie jest wykorzystywany w bieżącym kontekście wywołania.
Biorąc pod uwagę, że wykorzystanie adnotacji jest jedynie skrótem dla wykonania metody javax.naming.Context.lookup(String name) z parametrem name będącym nazwą zarządcy encji jak zapisano go w JNDI, więc nawet jeśli adnotacje nie są wspierane przez metody zwrotne, to i tak cały kontekst wykonania jest współdzielony, a tym samym i zawartość drzewa JNDI.
Lektura specyfikacji EJB 3.0 - JSR 220: Enterprise JavaBeansTM,Version 3.0 - EJB Core Contracts and Requirements upewnia nas w przekonaniu, że takie podejście, jakkolwiek niezalecane, jest teoretycznie dostępne.
W rozdziale 16.11.1.2 Programming Interfaces for Persistence Context References (str. 443) napisano:
The Bean Provider must use persistence context references to obtain references to a container-managed entity manager configured for a persistence unit as follows:
- Assign an entry in the enterprise bean's environment to the persistence context reference.
- The EJB specification recommends, but does not require, that all persistence context references be organized in the java:comp/env/persistence subcontexts of the bean's environment.
- Lookup the container-managed entity manager for the persistence unit in the enterprise bean's environment using the EJBContext lookup method or using the JNDI API.
Pora sprawdzić wiedzę w praktyce z dwoma serwerami aplikacyjnymi Apache Geronimo 2.1 (wersja rozwojowa) oraz GlassFish 2.1 b17.
Dostęp do EntityManager praktycznie - aplikacja demonstracyjna
Rozszerzenie projektu - ear-ejb-beanname
Na bazie mojego poprzedniego artykułu Element beanName w @EJB do rozróżnienia deklaracji ziaren EJB zbadamy dostępność zarządcy encji w drzewie JNDI w metodzie zwrotnej encji JPA.
Encja Slowo
W projekcie tworzymy nową encję Slowo (klasa pl.jaceklaskowski.beanname.entity.Slowo).
package pl.jaceklaskowski.beanname.entity;
import java.io.Serializable;
import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PostPersist;
@Entity
public class Slowo implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String slowo;
public Slowo() {
}
public Slowo(String slowo) {
this.slowo = slowo;
}
public void setId(int id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public String getSlowo() {
return slowo;
}
public void setSlowo(String slowo) {
this.slowo = slowo;
}
@PostPersist
protected void postPersist() {
try {
display((Context)new InitialContext().lookup("java:comp/env"));
// Pora na sprawdzenie, czy istnieje możliwość skorzystania z zarządcy encji
// związanego z ziarnem EJB, z którym jesteśmy związani
// UWAGA: Encja nie musi być związana z żadnym z ziaren EJB, tj. nic nie będzie w kontekście JNDI
((EntityManager)new InitialContext().lookup("java:comp/env/persistence/InventoryAppMgr")).persist(new WpisDoKapownika("Zapisano " + slowo));
} catch (Exception e) {
e.printStackTrace();
}
}
// Czy pracujemy z Geronimo czy GlassFish
private boolean glassfish = true;
private void display(Context ctx) {
try {
NamingEnumeration<Binding> ne = ctx.listBindings("");
while (ne.hasMore()) {
Binding b = (Binding) ne.next();
// HACK: Rozróżnij Geronimo vs GlassFish
String name = b.getName();
if (!name.startsWith("java:comp/env")) {
// Pracujemy z Geronimo
name = "java:comp/env" + "/" + name;
glassfish = false;
} else {
glassfish = true;
}
System.out.println(String.format("%s -> %s", name, b.getClassName()));
if (b.getObject() instanceof Context) {
if (glassfish) {
display((Context)b.getObject());
} else {
display(name, (Context)b.getObject());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void display(String parentContextName, Context ctx) {
try {
NamingEnumeration<Binding> ne = ctx.listBindings("");
while (ne.hasMore()) {
Binding b = (Binding) ne.next();
System.out.println(String.format("%s/%s -> %s", parentContextName, b.getName(), b.getClassName()));
if (b.getObject() instanceof Context) {
display(parentContextName + "/" + b.getName(), (Context)b.getObject());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Encja Slowo jest aktorem pierwszoplanowym w naszym przykładzie. Podczas zapisu do bazy danych za pomocą EntityManager.persist() metoda Slowo.postPersist() oznaczona adnotacją @PostPersist zostanie wywołana, która z kolei wyświetli bieżącego stanu drzewa JNDI oraz wywoła EntityManager.persist() na zarządcy encji pobranym z drzewa JNDI.
Encja WpisDoKapownika
package pl.jaceklaskowski.beanname.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PostPersist;
@Entity
public class WpisDoKapownika implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String wpis;
public WpisDoKapownika() {
}
public WpisDoKapownika(String wpis) {
this.wpis = wpis;
}
public void setId(int id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
@PostPersist
protected void postPersist() {
System.out.println("Wpis do kapownika o tresci: " + wpis + " zapisany");
}
}
Konfiguracja JPA - persistence.xml
Plik konfiguracyjny JPA - persistence.xml jest trywialny (poza wykorzystaniem parametrów konfiguracyjnych dla Apache OpenJPA - domyślny dostawca JPA w Geronimo - oraz TopLink JPA - domyślny dostawca JPA w GlassFish).
<?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="ear-ejb-beanname-ejbPU" transaction-type="JTA">
<jta-data-source>jdbc/sample</jta-data-source>
<non-jta-data-source>jdbc/sample</non-jta-data-source>
<properties>
<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(SchemaAction='add,deleteTableContents')"/>
<property name="toplink.ddl-generation" value="drop-and-create-tables"/>
</properties>
</persistence-unit>
</persistence>
Zmodyfikowane ziarno PolskoAngielskiSlownik
Zmodyfikowane ziarno PolskoAngielskiSlownik prezentuje kilka technik deklarowania i wykorzystania zależności w środowisku uruchomieniowym. Wszystkie zalezności są automatycznie zapisywane w drzewie JNDI w kontekście java:comp/env. Wykorzystanie metody javax.ejb.SessionContext.lookup(String name) jest skrótem dla odpowiedniego wywołania javax.naming.Context.lookup(String name) dla nazwy name, który jest relatywna w stosunku do java:comp/env.
Warto również zwrócić uwagę na użycie adnotacji @PersistenceContext, która umieszcza jednostkę trwałą w drzewie JNDI pod nazwą persistence/InventoryAppMgr.
package pl.jaceklaskowski.beanname.ejb;
import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import pl.jaceklaskowski.beanname.entity.Slowo;
@PersistenceContext(name="persistence/InventoryAppMgr", unitName="ear-ejb-beanname-ejbPU")
@Stateless(name="PolskoAngielskiSlownik")
public class PolskoAngielskiSlownik implements Slownik {
@PersistenceContext(name="entitymanager")
EntityManager em;
@Resource SessionContext ctx;
public String przetlumacz(String slowo) {
((EntityManager)ctx.lookup("persistence/InventoryAppMgr")).persist(new Slowo(slowo));
return "pl->ang: " + slowo;
}
}
Uruchomienie aplikacji
Uruchomienie aplikacji przynosi nam odpowiedź na pytanie o dostępność zarządcy encji w drzewie JNDI podczas wywołania metody oznaczonej adnotacją @PostPersist w serwerach Geronimo i GlassFish (przypomniam, że oba są certyfikowane na zgodność ze specyfikacją Java EE 5!).
Rozpoczniemy od uruchomienia przykładu na GlassFish, który jest referencyjną implementacja specyfikacji Java EE 5.
java:comp/env/pl.jaceklaskowski.beanname.faces.SlownikBean -> com.sun.enterprise.naming.java.javaURLContext
java:comp/env/pl.jaceklaskowski.beanname.faces.SlownikBean/angielskoPolskiSlownik -> $Proxy191
java:comp/env/pl.jaceklaskowski.beanname.faces.SlownikBean/polskoAngielskiSlownik -> $Proxy191
javax.naming.NameNotFoundException: No object bound to name java:comp/env/persistence/InventoryAppMgr
at com.sun.enterprise.naming.NamingManagerImpl.lookup(NamingManagerImpl.java:834)
at com.sun.enterprise.naming.java.javaURLContext.lookup(javaURLContext.java:173)
at com.sun.enterprise.naming.SerialContext.lookup(SerialContext.java:337)
at javax.naming.InitialContext.lookup(InitialContext.java:351)
at pl.jaceklaskowski.beanname.entity.Slowo.postPersist(Slowo.java:62)
Uruchomienie aplikacji na Geronimo przynosi bardzo zaskakujące rezultaty:
23:25:27,406 INFO [Transaction] TX Required: Started transaction org.apache.geronimo.transaction.manager.TransactionImpl@110c73b Slowo ziarno utworzone (za pomoca konstruktora) java:comp/env/entitymanager -> java.lang.Object java:comp/env/pl.jaceklaskowski.beanname.ejb.PolskoAngielskiSlownik -> org.apache.xbean.naming.context.WritableContext$NestedWritableContext java:comp/env/pl.jaceklaskowski.beanname.ejb.PolskoAngielskiSlownik/ctx -> javax.naming.LinkRef java:comp/env/persistence -> org.apache.xbean.naming.context.WritableContext$NestedWritableContext java:comp/env/persistence/InventoryAppMgr -> java.lang.Object Wpis do kapownika o tresci: Zapisano ziarno zapisany 23:25:27,453 INFO [Transaction] TX Required: Committing transaction org.apache.geronimo.transaction.manager.TransactionImpl@110c73b
Wniosek płynie z tego taki, że jakkolwiek możliwe jest wykorzystanie zarządcy encji podczas wywołania metody zwrotnej encji na serwerze Apache Geronimo, to niekoniecznie musi to być możliwe na innym certyfikowanym serwerze aplikacji GlassFish. Wszystko zgodnie ze specyfikacją JPA:
In general, portable applications should not invoke EntityManager or Query operations, access other entity instances, or modify relationships in a lifecycle callback method.
Warto czytać specyfikacje!
Zmodyfikowany projekt ear-ejb-beanname dostępny jest do pobrania jako ear-ejb-beanname-postpersist.zip.
