.NETからJava EE6をやるようになって「えっ」と思ったことの1つとして
「EJBを使うとDBのトランザクション処理も自動でやってくれます」
という説明があります。え、自動…って何?怪しい…(-_-;みたいな(^^;
次期案件では以下のような商談-見積-製品…等ツリーになるテーブル構造があって、テーブル保存処理などでも上から下に流すようなことがあったりします。
なので、複数テーブルを保存する際のEJB挙動は確認しておきたいと思っていて、簡単なサンプルですが確認してみました。
本当に自動でトランザクション処理してロールバックとかしてくれるのか(^^;
試した内容
テーブル
とりあえず商談(Opportunity)と見積(Quote)の2つを定義して、見積は親の商談IDを外部キーで持っています。スキーマは以下のように超簡易です。
商談テーブル
見積テーブル
EntityとEJBの部分
ここら辺の生成は全てNetBeansの自動生成に甘えて
・Entityは「データベースからのエンティティ・クラス」で生成
・EJBは「エンティティ・クラスのセッションBean」で生成
してます。昨年作ったWebプロトもこのノリで作ってるので、検証のスモールモデルとしてはいいかなと。
具体的には以下のようなコードです。(Entityは省略)
AbstractFacade
EJBのCRUD処理を束ねるAbstractFacadeクラス。ってNetBeans生成のまんますぎですが…。
package jp.co.hoge.controller;
import java.util.List;
import javax.persistence.EntityManager;
public abstract class AbstractFacade<T> {
private Class<T> entityClass;
public AbstractFacade(Class<T> entityClass) {
this.entityClass = entityClass;
}
protected abstract EntityManager getEntityManager();
public void create(T entity) {
getEntityManager().persist(entity);
}
public void edit(T entity) {
getEntityManager().merge(entity);
}
public void remove(T entity) {
getEntityManager().remove(getEntityManager().merge(entity));
}
public T find(Object id) {
return getEntityManager().find(entityClass, id);
}
OpportunityFacade
商談テーブル用
package jp.co.hoge.controller;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import jp.co.hoge.entity.Opportunity;
@Stateless
public class OpportunityFacade extends AbstractFacade<Opportunity>{
@PersistenceContext(unitName = "HogePU")
private EntityManager em;
@Override
protected EntityManager getEntityManager() {
return em;
}
public OpportunityFacade(){
super(Opportunity.class);
}
}
QuoteFacadeは上記OpportunityがQuoteになっているだけでまんまなので省略します。
EJBでの実験用クラス
実験用に以下のようなEJBを用意しました。
package jp.co.hoge.controller;
import javax.ejb.Stateless;
import javax.inject.Inject;
import jp.co.hoge.entity.Opportunity;
import jp.co.hoge.entity.Quote;
@Stateless
public class TransactionEjb {
@Inject
OpportunityFacade oppFacade;
@Inject
QuoteFacade qteFacade;
public void saveOpportunityAndQuote(Opportunity opp, Quote qte){
oppFacade.create(opp);
qteFacade.create(qte);
}
}
商談と見積のEntityを受け取ってFacade経由でcreateするだけです。
で、ManagedBean側はCDIで
package jp.co.hoge.bean;
import javax.inject.Named;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import jp.co.hoge.controller.OpportunityFacade;
import jp.co.hoge.controller.QuoteFacade;
import jp.co.hoge.controller.TransactionEjb;
import jp.co.hoge.entity.Opportunity;
import jp.co.hoge.entity.Quote;
@Named(value = "transactionConfirmBean")
@RequestScoped
public class TransactionConfirmBean {
private String oppId;
private String oppNm;
private String qteId;
private String qteNm;
@Inject
TransactionEjb tranEjb;
public void save(){
Opportunity opp = new Opportunity();
opp.setId(oppId);
opp.setOpportunityName(oppNm);
Quote qte = new Quote();
qte.setId(qteId);
qte.setQuoteName(qteNm);
tranEjb.saveOpportunityAndQuote(opp, qte);
}
}
あとはJSFのxhtmlでテキストを配置したものを置いてプロパティと紐付け、CommandButtonのactionでsaveと紐付けをして完了です。ちなみにxhtmlは以下のような超簡素なものです。
<?xml version='1.0' encoding='UTF-8' ?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
<h:form>
<h:panelGrid id="pnlGrd" columns="2">
商談ID
<h:inputText id="oppIdTxt" value="#{transactionConfirmBean.oppId}" />
商談名
<h:inputText id="oppNmTxt" value="#{transactionConfirmBean.oppNm}" />
見積ID
<h:inputText id="qteIdTxt" value="#{transactionConfirmBean.qteId}" />
見積名
<h:inputText id="qteINmTxt" value="#{transactionConfirmBean.qteNm}" />
<h:commandButton id="btnCmnd" value="保存" action="#{transactionConfirmBean.save()}" />
</h:panelGrid>
</h:form>
</h:body>
</html>
保存が成功するパターン
まずは正常に商談、見積が保存される確認からです。実行して画面を起動します。
しょぼい画面ですいません…実験用です。
画面に適当に値を入れて…
ボタン押して保存!
DBはちゃんと保存されています。ちなみにEclipseLinkのログをFINESTにして、確認しました。
FINER: client acquired: 249677
FINER: TX binding to tx mgr, status=STATUS_ACTIVE
FINER: acquire unit of work: 2018654
FINEST: persist() operation called on: jp.co.hoge.entity.Opportunity[ id=1 ].
FINEST: persist() operation called on: jp.co.hoge.entity.Quote[ id=1 ].
FINER: TX beforeCompletion callback, status=STATUS_ACTIVE
FINER: begin unit of work commit
FINER: TX beginTransaction, status=STATUS_ACTIVE
FINEST: Execute query InsertObjectQuery(jp.co.hoge.entity.Opportunity[ id=1 ])
FINEST: Connection acquired from connection pool [default].
FINEST: reconnecting to external connection pool
詳細レベル(低): INSERT INTO OPPORTUNITY (ID, OPPORTUNITY_NAME) VALUES (?, ?)
bind => [2 parameters bound]
FINEST: Execute query InsertObjectQuery(jp.co.hoge.entity.Quote[ id=1 ])
詳細レベル(低): INSERT INTO QUOTE (ID, QUOTE_NAME, OPPORTUNITY_ID) VALUES (?, ?, ?)
bind => [3 parameters bound]
FINEST: Connection released to connection pool [default].
FINER: TX afterCompletion callback, status=COMMITTED
FINER: end unit of work commit
FINER: release unit of work
FINER: client released
上記で改行入れたのは、ブレークポイントで確認する際に、OpportunityFacadeのcreateを通り終わった所です。コミットされてます。
保存が失敗するパターン
失敗のさせ方が乱暴すぎですが、以下のような、わざとこけるコードをいれて実行しました(^^;いいのか。
package jp.co.hoge.controller;
import javax.ejb.Stateless;
import javax.inject.Inject;
import jp.co.hoge.entity.Opportunity;
import jp.co.hoge.entity.Quote;
@Stateless
public class TransactionEjb {
@Inject
OpportunityFacade oppFacade;
@Inject
QuoteFacade qteFacade;
public void saveOpportunityAndQuote(Opportunity opp, Quote qte){
oppFacade.create(opp);
int error = 10 / 0;
qteFacade.create(qte);
}
}
まあ実際には何かの処理でExceptionが出たり、という感じだと思いますが。
で、実行!EJBがちゃんとロールバックしてくれるなら、商談(opportunity)は保存されないはずです。
で、DBみると、おお!ロールバックされてるっぽい(^^)ホントか、ホントにロールバックしたのか…ととことん疑いながらログ見ると
FINER: client acquired: 30328196
FINER: TX binding to tx mgr, status=STATUS_ACTIVE
FINER: acquire unit of work: 618361
FINEST: persist() operation called on: jp.co.hoge.entity.Opportunity[ id=1 ].
FINER: TX afterCompletion callback, status=ROLLEDBACK
FINER: release unit of work
FINER: client released
WARNING: EJB5184:A system exception occurred during an invocation on EJB TransactionEjb, method: public void jp.co.hoge.controller.TransactionEjb.saveOpportunityAndQuote(jp.co.hoge.entity.Opportunity,jp.co.hoge.entity.Quote)
WARNING: javax.ejb.EJBException
at com.sun.ejb.containers.BaseContainer.processSystemException(BaseContainer.java:5215)
at com.sun.ejb.containers.BaseContainer.completeNewTx(BaseContainer.java:5113)
at com.sun.ejb.containers.BaseContainer.postInvokeTx(BaseContainer.java:4901)
ロールバックのステータスが!ちゃんとトランザクション処理してくれてるので安心しました。
やっちゃいそうなパターン
ちょっと気を付けないと、私みたいな初心者は以下のような呼び出しを書きそうです(^^;というか最初実は書いちゃいました…。
package jp.co.hoge.bean;
import javax.inject.Named;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import jp.co.hoge.controller.OpportunityFacade;
import jp.co.hoge.controller.QuoteFacade;
import jp.co.hoge.controller.TransactionEjb;
import jp.co.hoge.entity.Opportunity;
import jp.co.hoge.entity.Quote;
@Named(value = "transactionConfirmBean")
@RequestScoped
public class TransactionConfirmBean {
private String oppId;
private String oppNm;
private String qteId;
private String qteNm;
@Inject
OpportunityFacade oppFacade;
@Inject
QuoteFacade qteFacade;
public void save(){
Opportunity opp = new Opportunity();
opp.setId(oppId);
opp.setOpportunityName(oppNm);
Quote qte = new Quote();
qte.setId(qteId);
qte.setQuoteName(qteNm);
oppFacade.create(opp);
qteFacade.create(qte);
}
}
上記コードでcreateの間にわざとゼロ割コード入れると、当然ですが、商談だけは保存されます。
このコードはEJBではないので、EJBからすると、商談保存の前後でトランザクト、その後、見積保存の前後でトランザクトっていう形になるので、ロールバックとかありません。
C#からくると、最初はManagedBeanとEJBの境界というか、コンテナとかを意識しなかったりするので「EJBでトランザクションは自動」とだけ聞くと上記のようなことをやりそうな気が(^^;自分だけかな;
ふぅ。でもこれ便利だなー。