トランザクション
トランザクション準備 ①
その前にまずは『環境』を作成しましょう。 通常バッキングビーンでは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]した際に、複数のクラスとマッピングされていると管理が破綻します
・SampleService あらすじ
・SampleLogic 詳細
例えば先ほどの『保存』の場合は以下の様なイメージです。
・SampleService 「保存」処理には、[処理A][処理B][処理C]があるので、よろしく
・SampleLogic 「関数 syoriA()」「関数 syoriB()」「関数 syoriC()」で詳細処理を記述
余談ですが、良いプログラムの条件に『テストし易い』があり、テストし易いプログラムの1つに『細分化された関数』があります。
package service;
import jakarta.enterprise.context.Dependent;
@Dependent
public class SampleService extends CommonService {
private static final long serialVersionUID = 1L;
}
package service;
import java.io.Serializable;
import jakarta.enterprise.context.Dependent;
@Dependent
public class CommonService implements Serializable {
private static final long serialVersionUID = 1L;
}
package logic;
import jakarta.enterprise.context.Dependent;
@Dependent
public class SampleLogic extends CommonLogic {
private static final long serialVersionUID = 1L;
}
package logic;
import java.io.Serializable;
import jakarta.enterprise.context.Dependent;
@Dependent
public class CommonLogic implements Serializable {
private static final long serialVersionUID = 1L;
}
トランザクション準備 ②
※『Dao』はまだ作成しないでください それでは『Inject』で各クラスを繋げていきましょう。
まずは『SampleBean』に以下プロパティを追記してください。
private SampleService appService;
private SampleLogic appLogic;
そしてこの構造は複数作成してもビルドが通ります。 しかしちょっと待ってください!
この構造って『CommonService』から『CommonLogic』呼べませんよ。。 仕方ないので『CommonService』にも以下ロパティを追記してみましょう。
private CommonLogic commonLogic;
だって『CommonService』からインジェクトした『commonLogic』を辿って『SampleBean』に到着しますか?
しませんよね?
だって親クラスである以上、どのクラスに『extends』されるか分からないんです。 紹介しておいて出来ないのかよっ! そんな訳はありません。ちゃんと方法はあります。
要は『CommonService』が『CommonLogic』を《Inject》する際に、こう指示できれば良いのです。
『お前は「CommonLogic」だけ見てるんだぞ!』と。 つまり、一連の繋がりなんで気にしないで『CommonLogic』だけを見る様に仕向けるのです。
それを可能にするのが『@Qualifier(クオリファイア)』です。
では早速作成してみましょう。 新規にアノテーションを作成するので、パッケージは『annotation』にします。
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 {}
ではこのアノテーションを使って『CommonService』から『CommonLigic』を呼んでみましょう。
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;
}
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』を付与します。 どうですか。ビルドがちゃんと通るハズです。 そしてようやくトランザクションの解説準備が整いました。
JTAにおけるトランザクション管理方法
<!-- 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におけるトランザクション処理実行
まずは一番深い『Logic層』である『CommonLogic.java』『SampleLogic.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;
}
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;
}
}
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()』が発火した場合とします。
public String submit() {
appService.insertUser();
return SELF;
}
<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」でエラーになるのはプログラムが悪いので基本的に設定しないで良いかと
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();
}
※コンソールに「登録失敗~~~!」と表示されます




先ほど『CDI管理』がクラッシュします。
開発現場のあるあるで、最初に作成した構造はビルド通ったけど、同じ様な構造をもうひとつ作ったらクラッシュ!とお話しましたが『CDI管理』は本当に油断してるとすぐ破綻します。
簡単に説明すると『@ViewScoped』から始まった管理が『@Dependent』で下層までリレーしていきます。
この時、どのソースのどの『@Dependent』を見ても、上流の『@ViewScoped』まで辿れる必要があります。
なんて事も普通にあります。
これは2つ目の構造が、1つ目の構造の何処かのクラスに干渉した可能性が高いです。 何が言いたいかというと、今回のサンプル構造を『SampleBean』で作成したら『NextBean』でも作成してみましょう。