R e i - D r e a m
for Laravel
Guest
login

最終投稿日:2026年08月09日

トランザクション

トランザクション準備 ①

セレクトは成功しましたが、その他のSQLも試したいですよね。
その前にまずは『環境』を作成しましょう。
通常バッキングビーンではSQLに関する記述はしません。
それは『可読性』を上げるため、バッキングビーンでは主にイベントに関するメソッドを作成します。
例えば『保存』ボタンが押下された場合は以下の様にします。
バッキングビーン

public void touroku() {
    if (!this.appValid(dto)) {
        // バリデーションエラー
    }
    try {
        // 保存処理
        appService.save(dto);
    } catch(Exception e) {
        // エラー処理
    }
}

上記のイメージです。
『バリデーション』『サービス』の呼び出し程度で記述するのが理想です。
上記で出てくる『appService』は、サービス層にある『SampleService』となり、処理を渡しています。
因みに、私が好きな構成は以下です。
@ViewScoped        @Dependent            @Dependent
+------------+     +---------------+     +-------------+
| SampleBean +-----+ SampleService +-----+ SampleLogic |
+-----+------+     +-------+-------+     +-----+-------+
      |                   |                    |
      |            @Dependent            @Dependent          @Dependent
+-----+-----+     +-------+-------+      +-----+-------+     +---------+
| BaseBean  +-----+ CommonService +------+ CommonLogic +-----+ UserDao |
+-----------+     +---------------+      +-------------+     +----+----+
                                                                  |
                                                             +----+----+
                                                             | BaseDao |
                                                             +---------+
    
こんなに沢山クラスを作成したら、何処に何を記述して良いかわからない。
と、思いますよね?
でもこれ案外合理的な構造になってるんですよ。
当然『BaseBean』『CommonService』『CommonLogic』は基底クラスなので使い回します。
注目は『SampleBean』『SampleService』『SampleLogic』です。
この3つは実はセットです。
つまり『SampleService』は『SampleBean』からしか呼ばれないし、『SampleLogic』は『SampleService』からしか呼ばれません。
これはシステム的にも、こうしないと『CDI管理』がクラッシュします。
    ※[Inject]した際に、複数のクラスとマッピングされていると管理が破綻します
また運用的には、こう考えて下さい。
・SampleBean            目次
・SampleService        あらすじ
・SampleLogic            詳細
『本』をイメージすると分かりやすいかもです。
例えば先ほどの『保存』の場合は以下の様なイメージです。
・SampleBean            「保存」が押下されたので、処理よろしく
・SampleService        「保存」処理には、[処理A][処理B][処理C]があるので、よろしく
・SampleLogic            「関数 syoriA()」「関数 syoriB()」「関数 syoriC()」で詳細処理を記述
どうですか。可読性が爆上がりになってる気がしませんか?
余談ですが、良いプログラムの条件に『テストし易い』があり、テストし易いプログラムの1つに『細分化された関数』があります。
ポイント

先ほど『CDI管理』がクラッシュします。
とお話しましたが『CDI管理』は本当に油断してるとすぐ破綻します。
簡単に説明すると『@ViewScoped』から始まった管理が『@Dependent』で下層までリレーしていきます。
この時、どのソースのどの『@Dependent』を見ても、上流の『@ViewScoped』まで辿れる必要があります。

開発現場のあるあるで、最初に作成した構造はビルド通ったけど、同じ様な構造をもうひとつ作ったらクラッシュ!
なんて事も普通にあります。
これは2つ目の構造が、1つ目の構造の何処かのクラスに干渉した可能性が高いです。
何が言いたいかというと、今回のサンプル構造を『SampleBean』で作成したら『NextBean』でも作成してみましょう。

以下『SampleBean』からのサンプルコードとなります。
SampleService.java

package service;

import jakarta.enterprise.context.Dependent;

@Dependent
public class SampleService extends CommonService {
    private static final long serialVersionUID = 1L;

}

CommonService.java

package service;

import java.io.Serializable;
import jakarta.enterprise.context.Dependent;

@Dependent
public class CommonService implements Serializable {
private static final long serialVersionUID = 1L;

}

SampleLogic.java

package logic;

import jakarta.enterprise.context.Dependent;

@Dependent
public class SampleLogic extends CommonLogic {
    private static final long serialVersionUID = 1L;

}

CommonLogic.java

package logic;

import java.io.Serializable;
import jakarta.enterprise.context.Dependent;

@Dependent
public class CommonLogic implements Serializable {
private static final long serialVersionUID = 1L;

}

トランザクション準備 ②

さて、『SampleBean』『SampleService』『SampleLogic』が出来ましたか。
    ※『Dao』はまだ作成しないでください
それでは『Inject』で各クラスを繋げていきましょう。
まずは『SampleBean』に以下プロパティを追記してください。
@Inject
private SampleService appService;
次に『SampleService』に以下プロパティを追記してください。
@Inject
private SampleLogic appLogic;
これで『SampleBean』『SampleService』『SampleLogic』が繋がりました。
そしてこの構造は複数作成してもビルドが通ります。
しかしちょっと待ってください!
この構造って『CommonService』から『CommonLogic』呼べませんよ。。
仕方ないので『CommonService』にも以下ロパティを追記してみましょう。
@Inject
private CommonLogic commonLogic;
これで完璧な『SampleBlean』の各階層構造が整いました。
    嘘です。実はこれビルド通りません!
先ほども触れましたが『CDI管理』がクラッシュします。
だって『CommonService』からインジェクトした『commonLogic』を辿って『SampleBean』に到着しますか?
しませんよね?
だって親クラスである以上、どのクラスに『extends』されるか分からないんです。
    紹介しておいて出来ないのかよっ!
そんな訳はありません。ちゃんと方法はあります。
要は『CommonService』が『CommonLogic』を《Inject》する際に、こう指示できれば良いのです。
『お前は「CommonLogic」だけ見てるんだぞ!』と。
つまり、一連の繋がりなんで気にしないで『CommonLogic』だけを見る様に仕向けるのです。
それを可能にするのが『@Qualifier(クオリファイア)』です。
では早速作成してみましょう。
新規にアノテーションを作成するので、パッケージは『annotation』にします。
BaseLogic.java

package annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import jakarta.inject.Qualifier;

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface BaseLogic {}

ファイル名は任意ですが、今回は『CommonLogic』用となるので『BaseLogic』としました。
ではこのアノテーションを使って『CommonService』から『CommonLigic』を呼んでみましょう。
CommonService.java

package service;

import java.io.Serializable;

import annotation.BaseLogic;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import logic.CommonLogic;

@Dependent
public class CommonService implements Serializable {
    private static final long serialVersionUID = 1L;
    @Inject

    @BaseLogic
    private CommonLogic commonLogic;
}

CommonLogic.java

package logic;

import java.io.Serializable;

import annotation.BaseLogic;
import jakarta.enterprise.context.Dependent;

@BaseLogic
@Dependent
public class CommonLogic implements Serializable {
    private static final long serialVersionUID = 1L;
}

呼び出し側は、プロパティに『@BaseLogic』を付与し、
呼ばれる側はクラスに『@BaseLogic』を付与します。
どうですか。ビルドがちゃんと通るハズです。
    そしてようやくトランザクションの解説準備が整いました。

JTAにおけるトランザクション管理方法

J = Java、T = Transaction、A = APIの略です。
○ pom.xmlの設定
トランザクション管理するためには以下の依存関係を登録する必要があります。
pom.xml

<!-- Jakarta Transactions API (JTA) -->
<dependency>
    <groupId>jakarta.transaction</groupId>
    <artifactId>jakarta.transaction-api</artifactId>
    <version>2.0.1</version>
    <scope>provided</scope>
</dependency>

設定は簡単で、トランザクション管理したい『クラス』『メソッド』にアノテーションを付与するだけです。
書式は以下
    @Transactional(value = Transactional.TxType.【トランザクションタイプ】)
一般的に頻繁に使われるタイプが以下となります。
    @Transactional(value = Transactional.TxType.REQUIRED)
タイプの一覧と意味を以下にまとめます。
REQUIRED トランザクションがあればそれを引き継ぎ、なければ新規作成します
REQUIRES_NEW 呼び出し元の状態に関わらず、常に新しい独立したトランザクションを開始します
MANDATORY 既にトランザクションが存在することを強制します。なければ例外
SUPPORTS トランザクションがあれば引き継ぎますが、なければトランザクションなしで実行します
NOT_SUPPORTED トランザクションが存在していても一時中断し、常にトランザクションなしで実行します
NEVER トランザクションが存在していると例外を発生させます
色々な管理方法がありますが、一つ大事な事があります。それは、
    『ロールバック処理を意識的にしたい』です。
実はJTAの管理は『例外が送出されるとロールバックされる』事になっています。
ですので、制御したい場合は例外を送出して下さい。
ただし『』があります!
この例外はなんと『非チェック例外』しか適用されません。
    ※ try~catch記述しなくても怒られないやつ
つまり予期せぬ例外の代表『Exception』の場合は、シレっとコミットされます!
これを制御するのが『rollbackOn』属性です。
例えば以下の様に記述すると『Exception』の場合でもロールバックされます。
    @Transactional(rollbackOn = {Exception.class})
また逆に『非チェック例外』の場合でもコミットしたい場合は以下の様に記述します。
    @Transactional(dontRollbackOn = {RuntimeException.class})
まぁ一般的(個人的)には、以下の管理が良いかと思っています。
    @Transactional(value = Transactional.TxType.REQUIRED, rollbackOn = {Exception.class})

JTAにおけるトランザクション処理実行

それでは実際にトランザクションを設定してDB登録してみましょう。
まずは一番深い『Logic層』である『CommonLogic.java』『SampleLogic.java』のクラスに設定します。
CommonLogic.java

package logic;

import java.io.Serializable;
import annotation.BaseLogic;

import dao.UserDao;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;

@BaseLogic
@Dependent
public class CommonLogic implements Serializable {
    private static final long serialVersionUID = 1L;

    
    @Inject
    protected UserDao userDao;
}

SampleLogic.java

package logic;

import entity.User;
import jakarta.enterprise.context.Dependent;
import jakarta.transaction.Transactional;

@Dependent
@Transactional(value = Transactional.TxType.REQUIRED, rollbackOn = {Exception.class})
public class SampleLogic extends CommonLogic{
    private static final long serialVersionUID = 1L;

    
    public boolean insertUser(String name, String mail) {
        // users登録
        User ent = new User();
        ent.setMail(mail);
        ent.setPass("bar0000");
        ent.setName(name);
        try {
            userDao.create(ent);
            return true;
        } catch (Exception e) {
            System.out.println("登録失敗~~~!");
        }
        return false;
    }
}

次に『Service層』から呼び出すため『SampleService.java』を修正します。
SampleService.java

package service;

import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import logic.SampleLogic;

@Dependent
public class SampleService extends CommonService {
    private static final long serialVersionUID = 1L;
    @Inject
    private SampleLogic appLogic;

    
    public void insertUser() {
        if(appLogic.insertUser("bar太郎", "bar@bar.com")) {
            System.out.println("登録成功~~~!");
        }
    }
}

最後にバッキングビーンから呼び出します。
イベント『submit()』が発火した場合とします。
SampleBean.java

public String submit() {
    appService.insertUser();

    return SELF;
}

あ、それと『sample.xhtml』のAJAXはやめて同期通信にして下さいね。
sample.xhtml

<h:commandButton value="自画面遷移" actionListener="#{sampleBean.submit()}" />

それではビルドしてボタンを押下してみましょう。
どうですか?
エクリプスのコンソールに『登録成功~~~!』と表示されてますよね!
実際にMySQLに接続して確認してみて下さい。
さて、それではもう一度発火させたらどうなるでしょう?
テーブル『users』のDDLを覚えていますか?
そうです。メールアドレスは『一意制約』があります。
ではボタンを押下して発火させて下さい。
エラーになりました。
    てか、ガチのエラーです。
ポイント

JPAの遅延処理の罠。
実はJPAと言うかJAVAはメモリをとっても大切にする言語なので《CRUD》処理の記述をした段階では実行していません。
いわゆる『キューに登録』された状態です。

では何時実行されるのでしょうか?
それはトランザクション管理を抜けたタイミングです。
今回「try ~ catch」を記述したクラスは、まだトランザクション管理内でした。
よって、トランザクション管理を抜けた『Service層』でやっとSQL一意制約違反となりますが、もう誰もキャッチしてくれません。
結果的にガチのエラーとなります。
個人的には『Service層』も最終的にはトランザクション管理にしたいと思っているので、残るはバッキングビーンとなってしまいます。
ただし『トランザクション準備』の章でも触れたように、バッキングビーンにはゴチャゴチャとコードを記述したくはありません。
ではどうするか?
そうです!直ぐにSQLを発行すれば良いのです。
それを命令する便利なコマンドは以下です。
    【EntityManager】.flush();
これを『insert』『update』『delete』メソッドに追記しましょう。
    ※「select」でエラーになるのはプログラムが悪いので基本的に設定しないで良いかと

それでは《ポイント》の説明の通り『BaseDao.java』を修正します。
BaseDao.java

public void create(T entity) {
    em.persist(entity);

    em.flush();
}

public void update(T entity) {
    em.merge(entity);

    em.flush();
}

public void delete(T entity) {
    em.remove(em.contains(entity) ? entity : em.merge(entity));

    em.flush();
}

今度はちゃんと『SampleLogic』で捕捉されるハズです。
    ※コンソールに「登録失敗~~~!」と表示されます
きっぷる
ログインしてコメントを残そう!!


きっぷる