Challenge Engineer Life !

エンジニア人生を楽しみたい!仕事や趣味で学んだ技術的なことを書いていくブログです。

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トランザクションは自動」とだけ聞くと上記のようなことをやりそうな気が(^^;自分だけかな;

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

CDI インターセプト from Jar

複合コンポーネントに続き、その他でも再利用できるものをjarに固める作業を進めてます。
今日詰まったのはInject関連。

この作業の元となっているJavaEE6で作成したプロトタイプでは、インターセプトを使いました。
設定ファイルでInjectする内容を振り分けよう、というものです。

非常にしょぼい例ですが、パスワードの暗号化するロジックをこんな感じに定義してみたりしてます。

インタフェースを準備。

public interface IMessageDigest {
    /**
     * 指定文字列のメッセージダイジェストを取得する
     * @param encryptStr 暗号化する対象の文字列
     * @return メッセージダイジェスト文字列
     */
    public String getMessageDigest(String encryptStr);
}

例えばSHA256のアルゴリズムでハッシュ値を返すものを実装します。アノテーションとして@Alternativeを付けます。
その他、SHA512やMD5(もう現在安全ではないので利用禁止ですが)等も実装しておきます。

@Alternative
public class Sha256 implements IMessageDigest{
    
    /**
     * 指定文字列のSHA256によるメッセージダイジェストを取得する
     * @param encryptStr 暗号化する対象の文字列
     * @return メッセージダイジェスト文字列
     */
    @Override
    public String getMessageDigest(String encryptStr) {
        //SHA256でMD作る処理実装
    }
}

beans.xmlを用意して、

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
    <alternatives>
        <!-- パスワード暗号方式 -->
        <class>jp.co.hoge.encryption.Sha256</class> <!-- 今回のプロジェクトではこれ利用 -->
        <!-- <class>jp.co.hoge.encryption.Sha512</class> -->
    </alternatives>
</beans>

として、利用する所では

//beans.xmlで定義したクラスインスタンスがインジェクトされる
@Inject
IMessageDigest messageDigest;

public void hoge(){
  〜略〜
    String encryptPassword = messageDigest.getMessageDigest(password);
  〜略〜
}

といったような形です。

で、上記のインタフェースや実装クラスをjarに固めて、他のプロジェクトから利用してみたのですが、実行すると

重大: Exception while loading the app : WELD-001408 Unsatisfied dependencies for type [IMessageDigest] with qualifiers [@Default] at injection point [[field] @Inject jp.co.hogehoge.controller.HogeController.messageDigest]

のようなエラーが…。まさかJarの中にあるクラスのインスタンスはInjectできないのか…と焦りながらも、調べてみた所、同じことで詰まってる人が。

Injecting a bean from a different Jar in Weld

JarのMETA-INF配下にbeans.xmlを置きな、とのこと。インジェクト内容のスイッチングはもちろん利用側プロジェクトのbeans.xmlで定義したいので、とりあえず空のbeans.xmlをJar側のほうに置いただけですが無事動きました。

最近徐々に時間がなくなってきて「なぜこうすると動くのか」に注力する時間が減ってきてしまっている(>_<;
時間のあるうちに色々理解して「なんかわからないけど、こうしたら動いた」じゃなく「こうこうこうだから動く」という日記にしていきたい。

なぜCDIの@ApplicationScopedにはeager属性がないのだろう…

JSFに関して拠り所としてる書籍は金魚本と以下の本です。

この本はCDIについても記述があって、各項目レベルで「JSF」「CDI」とか書いてあるので便利です。英語ですが(^^;

で、今日はApplicationScopedに関してです。

View寄りの情報でアプリケーション起動時に取得してキャッシュしておきたいデータが出たため、初めてApplicationScopedを使ってみました。が…JSFの管理対象Bean(@ManagedBean)にはeager属性が存在していて、それをtrueにすることで実現できるのですが、なぜかCDIの管理対象Bean(@Named)にはeager属性がなく、同じことができませんでした。

この辺りの違いに少しとまどいます。
同じようなことを考える人はいるようで、StackOverflowで以下のようなQ&Aをみつけました。
http://stackoverflow.com/questions/7828987/jsf-named-bean-eager-application-scoped-aka-managedbeaneager-true

回答をみるとCDIのextensionを使えばできる、ような記述がありますが、今はまだあまりトリッキー(なのかな?)なことをできるほど力がないので、なるべく標準機能で実装したいです。

ということで、今まで基本的にバックBeanはCDIで統一してきたのですが、このケースだけはJSF管理対象を利用することにしました。

続・画面遷移に悩む

少し前に「画面遷移に悩む」を書いたのですが、そこからパートナーと一緒に色々探った結果、最終的には自前でCDIのViewScopeを作るのが最も自分たちの理想に近い動きをすることがわかりました。

参考にしたのはこちらのサイト
http://www.verborgh.be/articles/2010/01/06/porting-the-viewscoped-jsf-annotation-to-cdi/

Stackoverflowでも色々議論されていて
http://stackoverflow.com/questions/11832666/jsf-2-0-cdi-scopes-and-best-practises
http://stackoverflow.com/questions/4865047/view-scope-in-cdi-weld
等など、他にもたくさん出てきました。やはり皆、どういうスコープを使うのが良いのか悩んでいる様子。

MyFaceのCODIを使う、とか、JBoss Seam3を使う等の解決策もありましたが、自分達はまだそういったものを扱えるほどのレベルではないので積極的になれませんでした。
で、投稿回答の中に、Seamを使ってなくて普通のJSFとPrimefacesを使っている、と言う人が(今回自分たちが参考にした)上記のサイトをあげていて、実装してみたら理想に近い形となりました。

結局の所、単純にJSFの@ViewScopeをCDIで使えればいい、というものなので、なぜCDIの標準としてViewScopeがないのか疑問です。。。

それにしても割と単純なWebシステムのプロトタイプを作るだけなのに、学習コストが高いなぁという気がします。
ASP.NETならば、わりと悩まない話かと思います。

画面遷移に悩む

現在開発しているプロトタイプでは、画面xhtmlの裏にいるバックビーンはCDIの管理Beanを使用することで統一しました。

@Named(value = "xxxBean")...

Beanは画面で共通的に持つフィールド変数(画面IDやユーザ情報クラスなど)は抽象クラスで括りだし、実装しています。

public abstract class AbstractBaseBean implements Serializable{
    
    /**
     * 画面ID
     */
    private String screenId;

    ...
}

これを継承したCDI管理Beanを各画面に対して1対1となるように定義する形です。

 
@Named(value = "xxxBean")
@RequestScoped
public class XxxBean extends AbstractBaseBean implements Serializable{

   各画面の定義

}

で、悩ましいのがスコープと画面遷移です。

当初は単純にRequestScopeにしていました。画面遷移は自画面のBeanの関数を呼び出して、そこから抽象クラスの画面遷移処理へ遷移先の画面IDを渡して呼び出します。抽象クラスでは、ログ処理を行い、画面IDからURLを返す処理をします。

RequestScopeなので、画面遷移の際に自画面のBeanのコンストラクタ(@PostConstruct)が呼ばれます。
これはASP.NET的に簡単にポストバック判定ができるものと思っていました。
FacesContextにisPostbackなる関数もあったので。

しかしながら、記述してみると、なぜか上手く判定してくれません。なぜだ…。これは要詳細調査事項として保留中です。

仕方ないからSessionScopeにするか、と安易に考えそうですが、業務系で使うシステムなのでメモリ肥大は性能問題等に絡んでくるため好ましくないです。

CDIにはConversationScopeという便利なスコープもあるので、これを採用して、コンストラクタでBeginして、画面遷移でEndするようにしました。
しかし、ブラウザの戻るボタンが絡んでくると上手く動かないケースがあり、微妙です。

上記でやっていることは、JSFのViewScopeと同じなので、ViewScopeが欲しいのですが、CDIには標準ではないようです。なぜ…。

そんなこんなで、現在どのようにスコープを設定するか、中々悩ましい状況となっています。
みんなどうやっているんだろうか…。結構調べたのですが、なかなか「これだ!」という情報がないなぁ、という感じです。

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