読者です 読者をやめる 読者になる 読者になる

Challenge Java EE !

Java EEを中心に趣味や仕事における開発メモを書いています。

EJBで複数テーブル保存時の自動ロールバック挙動を確認してみました

Java EE 6 NetBeans CDI JSF EJB

トランザクション処理自動って…

.NETからJava EE6をやるようになって「えっ」と思ったことの1つとして

EJBを使うとDBのトランザクション処理も自動でやってくれます」

という説明があります。え、自動…って何?怪しい…(-_-;みたいな(^^;

次期案件では以下のような商談-見積-製品…等ツリーになるテーブル構造があって、テーブル保存処理などでも上から下に流すようなことがあったりします。
f:id:kikutaro777:20130130203255j:plain
なので、複数テーブルを保存する際のEJB挙動は確認しておきたいと思っていて、簡単なサンプルですが確認してみました。

本当に自動でトランザクション処理してロールバックとかしてくれるのか(^^;

試した内容

環境

・OS:Windows 7 Professional
・IDE:NetBeans 7.2.1
・DB:Java DB(Apache Derby?)
・O/R:EclipseLink

テーブル

とりあえず商談(Opportunity)と見積(Quote)の2つを定義して、見積は親の商談IDを外部キーで持っています。スキーマは以下のように超簡易です。

商談テーブル

商談ID 商談名

見積テーブル

見積ID 商談ID 見積名
EntityとEJBの部分

ここら辺の生成は全てNetBeansの自動生成に甘えて
・Entityは「データベースからのエンティティ・クラス」で生成
EJBは「エンティティ・クラスのセッションBean」で生成
してます。昨年作ったWebプロトもこのノリで作ってるので、検証のスモールモデルとしてはいいかなと。

具体的には以下のようなコードです。(Entityは省略)

AbstractFacade

EJBCRUD処理を束ねる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;

    //~ setterとgetterは省略 ~
        
    @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);
    }
}

あとはJSFxhtmlでテキストを配置したものを置いてプロパティと紐付け、CommandButtonのactionでsaveと紐付けをして完了です。ちなみにxhtmlは以下のような超簡素なものです。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<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>

保存が成功するパターン

まずは正常に商談、見積が保存される確認からです。実行して画面を起動します。
しょぼい画面ですいません…実験用です。

f:id:kikutaro777:20130130203806j:plain

画面に適当に値を入れて…

f:id:kikutaro777:20130130203814j:plain

ボタン押して保存!

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;

    //~ setterとgetterは省略 ~

    @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);
        
        //各々Beanから呼んじゃう
        oppFacade.create(opp);      
        qteFacade.create(qte);
    }
}

上記コードでcreateの間にわざとゼロ割コード入れると、当然ですが、商談だけは保存されます。
このコードはEJBではないので、EJBからすると、商談保存の前後でトランザクト、その後、見積保存の前後でトランザクトっていう形になるので、ロールバックとかありません。

C#からくると、最初はManagedBeanとEJBの境界というか、コンテナとかを意識しなかったりするので「EJBトランザクションは自動」とだけ聞くと上記のようなことをやりそうな気が(^^;自分だけかな;

ふぅ。でもこれ便利だなー。

にほんブログ村 IT技術ブログへ
にほんブログ村
にほんブログ村 IT技術ブログ Javaへ
にほんブログ村