環境構築
マッピングの話
JPQL の話
Criteria API の話
JPA とは
Java Persistence API の略。
O/R マッパー。
データベースアクセスに関する多くの処理が抽象化されていて、オブジェクトの世界にデータベースの話が入り込まないように設計されている印象。
それゆえに、クラスとテーブルを1対1で対応させたり、生 SQL を直接記述したりといった実装をしていた人には取っ付きにくいフレームワークだと思う。
しかし、「エリック・エヴァンスのドメイン駆動設計」の5章、6章で紹介されているようなパターン(Entity, Value Object, Repository, Aggregate)を守ろうとすると、 JPA の持つ機能は結構重要になると思っている(特にテーブルとのマッピング)。
Hello World
プロジェクトの作成
コンテキストルートが jpa
になるように NetBeans で Web アプリケーションのプロジェクトを作成する。
データベースの用意
データベースには MySQL を使用する。
環境は以下のような感じ。
項目 | 値 |
---|---|
サーバー | localhost |
ポート | 3306(デフォルト) |
データベース | test |
ユーザ | test |
以下 DDL で sample_table
テーブルを作成する。
CREATE TABLE `sample_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
以下の SQL でデータを投入する。
INSERT INTO `test`.`sample_table` (`value`) VALUES ('hoge');
INSERT INTO `test`.`sample_table` (`value`) VALUES ('fuga');
INSERT INTO `test`.`sample_table` (`value`) VALUES ('piyo');
データソースを GlassFish に登録する
こちら を参照。
JNDI 名は jdbc/Local_MySQL_test
で登録しておく。
エンティティを作成する
package sample.javaee.jpa.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="sample_table")
public class Sample implements Serializable {
@Id
private Long id;
private String value;
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Sample{" + "id=" + id + ", value=" + value + '}';
}
}
永続ユニットを定義する
- NetBeans 上でプロジェクトを右クリックして [新規] → [その他] を選択。
- [カテゴリ] で "持続性" を選択し、 [ファイル・タイプ] で "持続性ユニット" を選択し [次へ] をクリック。
- [接続性ユニット名] に
SampleUnit
と入力。 - [データソース] に先ほど作成した MySQL のデータソース(
jdbc/Local_MySQL_test
)を選択し、 [終了] をクリック。 -
src/conf
の下にpersistence.xml
が出力される(NetBeans 上は、「構成ファイル」に表示される)。 - 不要な記述などを削除して、整形する。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="SampleUnit">
<jta-data-source>jdbc/Local_MySQL_test</jta-data-source>
</persistence-unit>
</persistence>
- このままだと war にしたときに
persistence.xml
が既定の場所(WEB-INF/classes/META-INF/
)に配置されないので、プロジェクトの設定を修正する。 - プロジェクトを右クリックして、 [プロパティ] を選択。
- [カテゴリ] で "ソース" を選択。
- [ソース・パッケージ・フォルダ] の [フォルダの追加] を選択。
-
src/conf
を選択して [開く] をクリック。
- これで、
persistence.xml
がWEB-INF/classes/META-INF/
の下に配備されるようになる。
DBアクセス処理を実装する
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.HelloJpaEjb;
@WebServlet("/hello/*")
public class HelloJpaServlet extends HttpServlet {
@EJB
private HelloJpaEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
if (path.endsWith("/persist")) {
this.ejb.persist(req.getParameter("value"));
} else if (path.endsWith("/remove")) {
this.ejb.remove(this.getId(req));
} else {
this.ejb.print(this.getId(req));
}
}
private long getId(HttpServletRequest req) {
return Long.parseLong(req.getParameter("id"));
}
}
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class HelloJpaEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void persist(String value) {
Sample sample = new Sample();
sample.setValue(value);
this.em.persist(sample);
System.out.println("persist : " + sample);
}
public void remove(long id) {
Sample sample = this.em.find(Sample.class, id);
this.em.remove(sample);
System.out.println("remove : " + sample);
}
public void print(long id) {
Sample sample = this.em.find(Sample.class, id);
System.out.println(sample);
}
}
動作確認
http://localhost:8080/jpa/hello/persist?value=HelloJPA!!
にアクセスする。
情報: persist : Sample{id=null, value=HelloJPA!!}
sample_table
の中を確認してみる。
http://localhost:8080/jpa/hello?id=4
にアクセスする。
情報: Sample{id=4, value=HelloJPA!!}
http://localhost:8080/jpa/hello/remove?id=4
にアクセスする。
情報: remove : Sample{id=4, value=HelloJPA!!}
sample_table
の中を確認してみる。
説明
永続ユニット
<persistence-unit name="SampleUnit">
<jta-data-source>jdbc/Local_MySQL_test</jta-data-source>
</persistence-unit>
- JPA では、永続ユニット(
<persistence-unit>
タグ)という単位で DB 接続の設定を作成する。-
name
属性で指定した名前は、アプリケーションで永続ユニットを指定するときに使用する。 - 永続ユニットは複数定義することができる。
-
- 永続ユニットでは、トランザクションの管理をコンテナに任せるか、自力で制御するかを指定することができる。
- Java EE サーバー上にデプロイした場合は、特に指定をしない限りコンテナがトランザクションを管理する(
JTA
が採用される)。 - 指定する場合は、
<persistence-unit>
タグにtransaction-type
属性を追加して指定する(JTA
orRESOURCE_LOCAL
)。
- Java EE サーバー上にデプロイした場合は、特に指定をしない限りコンテナがトランザクションを管理する(
-
<jta-data-source>
タグでは、使用するデータソースの JNDI ルックアップ名を指定する。
persistence.xml の配置場所
persistence.xml
は、状況に応じて以下のいずれかの場所に配置しなければならない。
ケース | 配置場所 |
---|---|
persistence.xml を war ファイルにまとめる場合 | WEB-INF/classes/META-INF/persistence.xml |
persistence.xml を war とは別の jar に梱包する場合 | <jar ファイル>/META-INF/persistence.xml |
エンティティ
package sample.javaee.jpa.entity;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="sample_table")
public class Sample implements Serializable {
@Id
private Long id;
private String value;
}
- JPA では、テーブルとマッピングするクラスのことをエンティティと呼ぶ。
-
Java EE サーバー上であれば、クラスを
@Entity
でアノテートすることで自動でエンティティとして登録することができる。- Java EE 環境以外で Eclipse Link などを自力で使用する場合は、自動登録されない。
- その場合は、
persistence.xml
に<class>
タグや<jar-file>
タグを使って手動で登録しなければならない。
- DB のテーブル名がエンティティのクラス名と異なる場合は、
@Table
アノテーションでマッピングを定義する。
EntityManager
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class HelloJpaEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void persist(String value) {
Sample sample = new Sample();
sample.setValue(value);
this.em.persist(sample);
System.out.println("persist : " + sample);
}
public void remove(long id) {
Sample sample = this.em.find(Sample.class, id);
this.em.remove(sample);
System.out.println("remove : " + sample);
}
public void print(long id) {
Sample sample = this.em.find(Sample.class, id);
System.out.println(sample);
}
}
- データベースにアクセスする際は、
EntityManager
を使用する。 -
EntityManager
のインスタンスは、@PersistenceContext
アノテーションを使うことでコンテナが管理するオブジェクトのフィールドにインジェクションすることができる。- EJB だけでなく、 Servlet にもインジェクションできる。
-
@PersistenceContext
のname
属性で、使用する永続ユニットの名前を指定する。 -
EntityManager
には、データベースにアクセスするための CRUD 操作メソッドが定義されており、それを使ってエンティティの取得、登録、削除などを行うことができる。-
EntityManager#find(Class, Object)
で、キー情報を使ってエンティティを取得する。 -
EntityManager#persist(Object)
で、指定したオブジェクトに対応するレコードをテーブルに登録する。 -
EntityManager#remove(Object)
で、指定したオブジェクトに対応するレコードをテーブルから削除する。
-
レコードの更新
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class HelloJpaEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void persist(String value) {
Sample sample = new Sample();
sample.setValue(value);
this.em.persist(sample);
System.out.println("persist : " + sample);
}
public void remove(long id) {
Sample sample = this.em.find(Sample.class, id);
this.em.remove(sample);
System.out.println("remove : " + sample);
}
public void print(long id) {
Sample sample = this.em.find(Sample.class, id);
System.out.println(sample);
}
+ public void update(long id, String value) {
+ Sample sample = this.em.find(Sample.class, id);
+ sample.setValue(value);
+
+ System.out.println("update : " + sample);
+ }
}
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.HelloJpaEjb;
@WebServlet("/hello/*")
public class HelloJpaServlet extends HttpServlet {
@EJB
private HelloJpaEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
if (path.endsWith("/persist")) {
- this.ejb.persist(req.getParameter("value"));
+ this.ejb.persist(this.getValue(req));
} else if (path.endsWith("/remove")) {
this.ejb.remove(this.getId(req));
+ } else if (path.endsWith("/update")) {
+ this.ejb.update(this.getId(req), this.getValue(req));
+
} else {
this.ejb.print(this.getId(req));
}
}
+ private String getValue(HttpServletRequest req) {
+ return req.getParameter("value");
+ }
private long getId(HttpServletRequest req) {
return Long.parseLong(req.getParameter("id"));
}
}
http://localhost:8080/jpa/hello?id=2
にアクセスする。
情報: Sample{id=2, value=fuga}
http://localhost:8080/jpa/hello/update?id=2&value=UpdateValue
にアクセスする。
情報: update : Sample{id=2, value=UpdateValue}
http://localhost:8080/jpa/hello?id=2
にアクセスする。
情報: Sample{id=2, value=UpdateValue}
JPA では、 DB のレコードを更新するために UPDATE 文を明示的に実行することはない。
JPA でのレコードの更新は、 find()
で取得したオブジェクトのフィールドを書き換えるだけでいい。
トランザクションがコミットされるタイミングで、 EntityManager がデータベースに変更を反映してくれる。
たぶん、 SQL をガリガリ書く実装をしていた人が JPA をやり始めて、一番最初に面食らう仕様だと思う。
最初は、ちょっとフィールドを変更しただけで DB に反映されることに対する不安感や不信感があるかもしれない。
しかし、この仕組みがあると実装からデータベース(インフラストラクチャ)に関する処理を取り除くことができ、よりドメインモデルに集中することができる(はず)。
ただし、ドメインモデルに重きをおくことはパフォーマンスとのトレードオフな部分もある。
O/R マッパーに関するパフォーマンスの問題とその対策については、以下のスライドが分かりやすいです。
EntityManager によるオブジェクトの管理
JPA では、エンティティオブジェクトの状態が EntityManager によって管理・監視されている。
EntityManager がエンティティオブジェクトを監視していることで、前述のようにフィールドを書き換えただけでデータベースに変更を反映することができている。
逆にいうと、 EntityManager に管理されていないオブジェクトは、フィールドを変更してもデータベースに反映されることはない。
JPA では、エンティティオブジェクトが EntityManager に管理されているかどうか、というのを意識するのが重要になる。
新しいエンティティオブジェクトを作成する
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
public void test() {
Sample sample = new Sample();
}
}
- 普通に
new
したオブジェクトは、当然 EntityManager には管理されていない。 - この状態のエンティティオブジェクトを、 NEW 状態 と呼ぶ。
NEW 状態のオブジェクトを EntityManager の管理下に置く
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
+ import javax.persistence.EntityManager;
+ import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
+ @PersistenceContext(unitName="SampleUnit")
+ private EntityManager em;
public void test() {
Sample sample = new Sample();
+
+ this.em.persist(sample);
}
}
-
EntityManager#persist(Object)
によって、エンティティオブジェクトを EntityManager の管理下に置くことができる。 - この状態のエンティティオブジェクトを MANAGED 状態 と呼ぶ。
- MANAGED 状態のオブジェクトは、 DB のレコードと同期がとられるようになる。
- NEW 状態のオブジェクトが MANAGED 状態になった場合、 DB と同期をとるためレコードが新規に登録される。
- この状態のオブジェクトのフィールドが変更されると、 DB と同期をとるため UPDATE が実行される。
- ※実際に同期がとられるのは、トランザクションがコミットされるとき。
- 既に DB に存在するレコードをエンティティオブジェクトとして取得し、 EntityManager の管理下に置きたい場合は、
EntityManager#find(Class, Object)
メソッドを使用する。
DB に記録されているエンティティの情報を破棄する
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void test() {
Sample sample = new Sample();
this.em.persist(sample);
+ this.em.remove(sample);
}
}
-
EntityManger#remove(Object)
メソッドに MANAGED 状態のエンティティオブジェクトを渡すことで、エンティティを DB から破棄することができる。- ※実際に破棄されるのは、トランザクションがコミットされるとき。
- この状態のエンティティオブジェクトを REMOVED 状態 と呼ぶ。
- REMOVED 状態のエンティティオブジェクトを再び MANAGED 状態に戻したい場合は、再度
persist()
メソッドを使用する。
EntityManager の管理下から外す
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void test() {
Sample sample = new Sample();
this.em.persist(sample);
- this.em.remove(sample);
+ this.em.clear();
}
}
- 以下のいずれかの条件を満たすと、エンティティオブジェクトは EntityManager の管理下から外される。
- トランザクションがコミットされる。
-
EntityManager#clear()
メソッドが実行される。
- この状態のエンティティオブジェクトを DETACHED 状態 と呼ぶ。
- DETACHED 状態になったオブジェクトは、 DB には反映されない。
- 上記実装の場合、
persist()
しているがトランザクションがコミットされる前にclear()
しているので、エンティティオブジェクトは DB と同期されず、レコードは登録されない。
- 上記実装の場合、
手動でデータベースに変更を反映させる
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void test() {
Sample sample = new Sample();
this.em.persist(sample);
+ this.em.flush();
this.em.clear();
}
}
-
EntityManager#flush()
メソッドを実行すると、明示的にエンティティの状態を DB に反映させることができる。 - 上記の場合、
clear()
の前にflush()
が実行されるので、 DB にレコードが登録される。
DETACHED 状態のオブジェクトを MANAGED 状態に戻す
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void test() {
Sample sample = new Sample();
this.em.persist(sample);
this.em.flush();
this.em.clear();
+ Sample managed = this.em.merge(sample);
+ managed.setValue("こちらの値が反映される");
+ sample.setValue("こちらの値は反映されない");
}
}
-
EntityManager#merge(Object)
メソッドを使うことで、 DETACHED 状態のエンティティオブジェクトを MANAGED 状態に戻すことができる。 - NEW 状態のエンティティオブジェクトを渡すこともでき、その場合はレコードが追加される。
- 引数で渡したエンティティオブジェクトは MANAGED 状態にならない。
- 代わりに、戻り値のオブジェクトが MANAGED 状態になっている(こちらのオブジェクトに変更を加えると、 DB に反映される)。
- 戻り値のオブジェクトは、引数で渡したオブジェクトのコピー。
MANAGED 状態のオブジェクトを DB の最新の状態で更新する
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class EntityStateManagementEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void test() {
Sample sample = new Sample();
this.em.persist(sample);
this.em.flush();
this.em.clear();
Sample managed = this.em.merge(sample);
managed.setValue("こちらの値が反映される");
sample.setValue("こちらの値は反映されない");
+ Sample hoge = this.em.find(Sample.class, 1L);
+ hoge.setValue("test");
+
+ this.em.refresh(hoge);
+
+ System.out.println("hoge=" + hoge);
}
}
http://localhost:8080/jpa/state
にアクセスする。
情報: hoge=Sample{id=1, value=hoge}
-
EntityManager#refresh(Object)
メソッドを使うと、指定したエンティティの状態を、現在の DB の状態で同期させることができる。
まとめ
○ ◎
new | ^ GC
v |
+---------+
| NEW | <--------------+
+---------+ |
find/JPQL | |
○----------------------+ | |
| | persist |
v v |
+---------+ merge +---------+ remove +---------+
| | -------> | | --------> | |
|DETACHED | <------- | MANAGED | | REMOVED |
| | clear | | <-+ | |
+---------+ detach +---------+ | +---------+
^ | |
| +------+
| refresh
| setter
v
+-------------+
+-------------+
| Database |
+-------------+
元ネタ:JPAエンティティのライフサイクルをテキストで書いてみた - Qiita
トランザクションの制御
Java EE サーバー上で、トランザクションタイプが JTA の永続ユニットを使用している場合、トランザクションの制御は EJB コンテナに任せることができる。
EJB のメソッドが実行されると、 EJB コンテナは自動でメソッドの前後にトランザクション境界を設ける。
つまり、メソッドの開始前にトランザクションが開始され、メソッドが終了するとトランザクションがコミットされる。
この動作は、アノテーションを使って細かく制御することができる。
アノテーションで調整する
デフォルトの動き
package sample.javaee.jpa.servlet;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.TransactionSynchronizationRegistry;
import sample.javaee.jpa.ejb.TransactionEjb;
@WebServlet("/transaction/*")
public class TransactionServlet extends HttpServlet {
@EJB
private TransactionEjb ejb;
@Resource
private TransactionSynchronizationRegistry tx;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
System.out.println("[TransactionServlet] tx.id=" + this.tx.getTransactionKey());
if (path.endsWith("/default")) {
this.ejb.defaultTransactionManagement();
}
}
}
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
public class TransactionEjb {
@Resource
private TransactionSynchronizationRegistry tx;
@EJB
private DefaultTxEjb defaultTxEjb;
public void defaultTransactionManagement() {
System.out.println("[TransactionEjb] tx.key=" + tx.getTransactionKey());
this.defaultTxEjb.method();
}
}
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
public class DefaultTxEjb {
@Resource
private TransactionSynchronizationRegistry tx;
public void method() {
System.out.println("[DefaultTxEjb] tx.key=" + tx.getTransactionKey());
}
}
http://localhost:8080/jpa/transaction/default
にアクセスする。
情報: [TransactionServlet] tx.id=null
情報: [TransactionEjb] tx.key=JavaEETransactionImpl: txId=9 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@1e8e0a9]
情報: [DefaultTxEjb] tx.key=JavaEETransactionImpl: txId=9 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@1e8e0a9]
-
txId
で、現在のスレッドが紐付けられているトランザクションを識別することができる。 - EJB のメソッドが実行された時点でトランザクションが自動で開始されている。
- EJB 内から別の EJB のメソッドが実行された場合、同じトランザクションが引き継がれている。
常に新しいトランザクションを開始させる
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public class RequiresNewTxEjb {
@Resource
private TransactionSynchronizationRegistry tx;
public void method() {
System.out.println("[RequiresNewTxEjb] tx.key=" + tx.getTransactionKey());
}
}
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
public class TransactionEjb {
@Resource
private TransactionSynchronizationRegistry tx;
@EJB
private DefaultTxEjb defaultTxEjb;
+ @EJB
+ private RequiresNewTxEjb requiresNewTxEjb;
public void defaultTransactionManagement() {
- System.out.println("[TransactionEjb] tx.key=" + tx.getTransactionKey());
+ this.printTxId();
this.defaultTxEjb.method();
}
+ public void requiresNew() {
+ this.printTxId();
+ this.requiresNewTxEjb.method();
+ }
+
+ private void printTxId() {
+ System.out.println("[TransactionEjb] tx.key=" + tx.getTransactionKey());
+ }
}
package sample.javaee.jpa.servlet;
(略)
@WebServlet("/transaction/*")
public class TransactionServlet extends HttpServlet {
@EJB
private TransactionEjb ejb;
@Resource
private TransactionSynchronizationRegistry tx;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
System.out.println("[TransactionServlet] tx.id=" + this.tx.getTransactionKey());
if (path.endsWith("/default")) {
this.ejb.defaultTransactionManagement();
+ } else if (path.endsWith("/requires-new")) {
+ this.ejb.requiresNew();
}
}
}
http://localhost:8080/jpa/transaction/requires-new
にアクセスする。
情報: [TransactionServlet] tx.id=null
情報: [TransactionEjb] tx.key=JavaEETransactionImpl: txId=10 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@3593ef74]
情報: [RequiresNewTxEjb] tx.key=JavaEETransactionImpl: txId=11 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@9c5c512]
-
@TransactionAttribute
でクラスをアノテートし、TransactionAttributeType.REQUIRES_NEW
を指定する。- メソッドをアノテートすることも可能。
- デフォルトは、
TransactionAttributeType.REQUIRED
を指定したのと同じになる。
- すると、その EJB のメソッドを実行すると常に新しいトランザクションが開始されるようになる。
-
txId=10
のトランザクションは一時中断されていて、txId=11
のトランザクションが終了したあとに再開される。
呼び出し元でトランザクションが開始されていれば、トランザクションを引き継ぐ
開始されていない場合は、トランザクションを開始しない
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public class SupportsTxEjb {
@Resource
private TransactionSynchronizationRegistry tx;
public void method() {
System.out.println("[SupportsTxEjb] tx.key=" + tx.getTransactionKey());
}
}
+ @EJB
+ private SupportsTxEjb supportsTxEjb;
+ public void supports() {
+ this.printTxId();
+ this.supportsTxEjb.method();
+ }
package sample.javaee.jpa.servlet;
(略)
+ import sample.javaee.jpa.ejb.SupportsTxEjb;
@WebServlet("/transaction/*")
public class TransactionServlet extends HttpServlet {
@EJB
private TransactionEjb ejb;
+ @EJB
+ private SupportsTxEjb supportsTxEjb;
@Resource
private TransactionSynchronizationRegistry tx;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
System.out.println("[TransactionServlet] tx.id=" + this.tx.getTransactionKey());
if (path.endsWith("/default")) {
this.ejb.defaultTransactionManagement();
} else if (path.endsWith("/requires-new")) {
this.ejb.requiresNew();
+ } else if (path.endsWith("/supports")) {
+ this.ejb.supports();
+ this.supportsTxEjb.method();
}
}
}
http://localhost:8080/jpa/transaction/supports
にアクセスする。
情報: [TransactionServlet] tx.id=null
情報: [TransactionEjb] tx.key=JavaEETransactionImpl: txId=13 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@1f631ce3]
情報: [SupportsTxEjb] tx.key=JavaEETransactionImpl: txId=13 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@1f631ce3]
情報: [SupportsTxEjb] tx.key=null
-
SUPPORTS
を指定すると、呼び出し元でトランザクションが開始されていれば、そのトランザクションを引き継ぐ。 - 呼び出し元でトランザクションが開始されていない場合は、トランザクションを開始せずにメソッドを実行する。
開始されていない場合、例外をスローさせる
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public class MandatoryTxEjb {
@Resource
private TransactionSynchronizationRegistry tx;
public void method() {
System.out.println("[MandatoryTxEjb] tx.key=" + tx.getTransactionKey());
}
}
+ @EJB
+ private MandatoryTxEjb mandatoryTxEjb;
+ public void mandatory() {
+ this.printTxId();
+ this.mandatoryTxEjb.method();
+ }
package sample.javaee.jpa.servlet;
(略)
+ import sample.javaee.jpa.ejb.MandatoryTxEjb;
@WebServlet("/transaction/*")
public class TransactionServlet extends HttpServlet {
@EJB
private TransactionEjb ejb;
@EJB
private SupportsTxEjb supportsTxEjb;
+ @EJB
+ private MandatoryTxEjb mandatoryTxEjb;
@Resource
private TransactionSynchronizationRegistry tx;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
System.out.println("[TransactionServlet] tx.id=" + this.tx.getTransactionKey());
if (path.endsWith("/default")) {
this.ejb.defaultTransactionManagement();
} else if (path.endsWith("/requires-new")) {
this.ejb.requiresNew();
} else if (path.endsWith("/supports")) {
this.ejb.supports();
this.supportsTxEjb.method();
+ } else if (path.endsWith("/mandatory")) {
+ this.ejb.mandatory();
+ this.mandatoryTxEjb.method();
}
}
}
http://localhost:8080/jpa/transaction/mandatory
にアクセスする。
情報: [TransactionServlet] tx.id=null
情報: [TransactionEjb] tx.key=JavaEETransactionImpl: txId=14 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@2fc0f5a4]
情報: [MandatoryTxEjb] tx.key=JavaEETransactionImpl: txId=14 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@2fc0f5a4]
警告: A system exception occurred during an invocation on EJB MandatoryTxEjb, method: public void sample.javaee.jpa.ejb.MandatoryTxEjb.method()
警告: javax.ejb.TransactionRequiredLocalException
at com.sun.ejb.containers.EJBContainerTransactionManager.preInvokeTx(EJBContainerTransactionManager.java:235)
(以下略)
-
MANDATORY
を指定すると、呼び出し元でトランザクションが開始していない場合、例外をスローさせることができる。
呼び出し元でトランザクションが開始されている場合、例外をスローさせる
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
@TransactionAttribute(TransactionAttributeType.NEVER)
public class NeverTxEjb {
@Resource
private TransactionSynchronizationRegistry tx;
public void method() {
System.out.println("[NeverTxEjb] tx.key=" + tx.getTransactionKey());
}
}
+ @EJB
+ private NeverTxEjb neverTxEjb;
+ public void never() {
+ this.printTxId();
+ this.neverTxEjb.method();
+ }
package sample.javaee.jpa.servlet;
(略)
+ import sample.javaee.jpa.ejb.NeverTxEjb;
@WebServlet("/transaction/*")
public class TransactionServlet extends HttpServlet {
@EJB
private TransactionEjb ejb;
@EJB
private SupportsTxEjb supportsTxEjb;
@EJB
private MandatoryTxEjb mandatoryTxEjb;
+ @EJB
+ private NeverTxEjb neverTxEjb;
@Resource
private TransactionSynchronizationRegistry tx;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
System.out.println("[TransactionServlet] tx.id=" + this.tx.getTransactionKey());
if (path.endsWith("/default")) {
this.ejb.defaultTransactionManagement();
} else if (path.endsWith("/requires-new")) {
this.ejb.requiresNew();
} else if (path.endsWith("/supports")) {
this.ejb.supports();
this.supportsTxEjb.method();
} else if (path.endsWith("/mandatory")) {
this.ejb.mandatory();
this.mandatoryTxEjb.method();
+ } else if (path.endsWith("/never")) {
+ this.neverTxEjb.method();
+ this.ejb.never();
}
}
}
http://localhost:8080/jpa/transaction/never
にアクセスする。
情報: [TransactionServlet] tx.id=null
情報: [NeverTxEjb] tx.key=null
情報: [TransactionEjb] tx.key=JavaEETransactionImpl: txId=17 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@59f0d1fa]
警告: A system exception occurred during an invocation on EJB NeverTxEjb, method: public void sample.javaee.jpa.ejb.NeverTxEjb.method()
警告: javax.ejb.EJBException: EJB cannot be invoked in global transaction
at com.sun.ejb.containers.EJBContainerTransactionManager.preInvokeTx(EJBContainerTransactionManager.java:277)
(以下略)
-
NEVER
を指定すると、呼び出し元でトランザクションが開始されていると例外をスローさせることができる。
トランザクションを一切使用させないようにする
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.transaction.TransactionSynchronizationRegistry;
@Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class NotSupportedTxEjb {
@Resource
private TransactionSynchronizationRegistry tx;
public void method() {
System.out.println("[NotSupportedTxEjb] tx.key=" + tx.getTransactionKey());
}
}
+ @EJB
+ private NotSupportedTxEjb notSupportedTxEjb;
+ public void notSupported() {
+ this.printTxId();
+ this.notSupportedTxEjb.method();
+ }
package sample.javaee.jpa.servlet;
(略)
+ import sample.javaee.jpa.ejb.NotSupportedTxEjb;
@WebServlet("/transaction/*")
public class TransactionServlet extends HttpServlet {
@EJB
private TransactionEjb ejb;
@EJB
private SupportsTxEjb supportsTxEjb;
@EJB
private MandatoryTxEjb mandatoryTxEjb;
@EJB
private NeverTxEjb neverTxEjb;
+ @EJB
+ private NotSupportedTxEjb notSupportedTxEjb;
@Resource
private TransactionSynchronizationRegistry tx;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
System.out.println("[TransactionServlet] tx.id=" + this.tx.getTransactionKey());
if (path.endsWith("/default")) {
this.ejb.defaultTransactionManagement();
} else if (path.endsWith("/requires-new")) {
this.ejb.requiresNew();
} else if (path.endsWith("/supports")) {
this.ejb.supports();
this.supportsTxEjb.method();
} else if (path.endsWith("/mandatory")) {
this.ejb.mandatory();
this.mandatoryTxEjb.method();
} else if (path.endsWith("/never")) {
this.neverTxEjb.method();
this.ejb.never();
+ } else if (path.endsWith("/not-supported")) {
+ this.ejb.notSupported();
+ this.notSupportedTxEjb.method();
}
}
}
http://localhost:8080/jpa/transaction/not-supported
にアクセスする。
情報: [TransactionServlet] tx.id=null
情報: [TransactionEjb] tx.key=JavaEETransactionImpl: txId=15 nonXAResource=null jtsTx=null localTxStatus=0 syncs=[com.sun.ejb.containers.ContainerSynchronization@43def44e]
情報: [NotSupportedTxEjb] tx.key=null
情報: [NotSupportedTxEjb] tx.key=null
-
NOT_SUPPORTED
を指定すると、既にトランザクションが開始されている場合は、そのトランザクションを中断させてからメソッドが実行される。-
NOT_SUPPORTED
が適用されたメソッドが終了した後で、中断されていたトランザクションは再開される。
-
- トランザクションが開始されていない場合は、そのままメソッドが実行される。
まとめ
呼び出し元のトランザクション→ | なし | あり |
---|---|---|
REQUIRED | Tx 開始 | 引き継ぐ |
REQUIRES_NEW | Tx 開始 | Tx 開始 |
SUPPORTES | Tx なし | 引き継ぐ |
MANDATORY | 例外 | 引き継ぐ |
NEVER | Tx なし | 例外 |
NOT_SUPPORTED | Tx なし | Tx なし(中断) |
ロールバック
例外によるロールバック
package sample.javaee.jpa.exception;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class MyRollbackException extends Exception {
}
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
import sample.javaee.jpa.exception.MyRollbackException;
@Stateless
public class ExceptionRollbackEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void throwException() throws Exception {
this.insertEntity("exception");
throw new Exception("test exception");
}
public void throwRuntimeException() {
this.insertEntity("runtime exception");
throw new RuntimeException("test runtime exception");
}
public void throwMyRollbackException() throws MyRollbackException {
this.insertEntity("my rollback exception");
throw new MyRollbackException();
}
private void insertEntity(String value) {
Sample sample = new Sample();
sample.setValue(value);
this.em.persist(sample);
}
}
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.ExceptionRollbackEjb;
@WebServlet("/ex-rollback/*")
public class ExceptionRollbackServlet extends HttpServlet {
@EJB
private ExceptionRollbackEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String path = req.getRequestURI();
try {
if (path.endsWith("/runtime")) {
this.ejb.throwRuntimeException();
} else if (path.endsWith("/my-exception")) {
this.ejb.throwMyRollbackException();
} else {
this.ejb.throwException();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
http://localhost:8080/jpa/ex-rollback
にアクセスする。
重大: java.lang.Exception: test exception
at sample.javaee.jpa.ejb.ExceptionRollbackEjb.throwException(ExceptionRollbackEjb.java:16)
(以下略)
sample_table
を確認する。
レコードが登録されている。
http://localhost:8080/jpa/ex-rollback/runtime
にアクセスする。
警告: javax.ejb.EJBException
at com.sun.ejb.containers.EJBContainerTransactionManager.processSystemException(EJBContainerTransactionManager.java:748)
(中略)
Caused by: java.lang.RuntimeException: test runtime exception
at sample.javaee.jpa.ejb.ExceptionRollbackEjb.throwRuntimeException(ExceptionRollbackEjb.java:21)
(以下略)
sample_table
を確認する。
レコードは登録されていない。
http://localhost:8080/jpa/ex-rollback/my-exception
にアクセスする。
重大: sample.javaee.jpa.exception.MyRollbackException
at sample.javaee.jpa.ejb.ExceptionRollbackEjb.throwMyRollbackException(ExceptionRollbackEjb.java:27)
(以下略)
sample_table
を確認する。
レコードは登録されていない。
-
Exception
を継承した例外がスローされた場合、トランザクションはロールバックされない。-
@ApplicationException
で例外クラスをアノテートしrollback=true
を設定すると、ロールバックさせることができる。
-
-
RuntimeException
を継承した例外がスローされた場合、トランザクションはロールバックされる。
手動ロールバック
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.Sample;
@Stateless
public class ManualRollbackEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
@Resource
private SessionContext context;
public void method() {
Sample sample = this.em.find(Sample.class, 2L);
sample.setValue("update");
this.context.setRollbackOnly();
}
}
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.ManualRollbackEjb;
@WebServlet("/manual-rollback")
public class ManualRollbackServlet extends HttpServlet {
@EJB
private ManualRollbackEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
this.ejb.method();
}
}
http://localhost:8080/jpa/manual-rollback
にアクセスする。
sample_table
を確認する。
-
SessionContext#setRollbackOnly()
を実行することで、現在のトランザクションを必ずロールバックさせるように指定できる。 -
SessionContext
は、@Resource
アノテーションを使って EJB にインジェクションする。
トランザクションを手動で管理する
package sample.javaee.jpa.ejb;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import sample.javaee.jpa.entity.Sample;
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class BeanManagementEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
@Resource
private UserTransaction tx;
public void method(boolean rollback) {
try {
this.tx.begin();
Sample sample = new Sample();
sample.setValue("BMT");
this.em.persist(sample);
if (rollback) {
this.tx.rollback();
System.out.println("rollback");
} else {
this.tx.commit();
System.out.println("commit");
}
} catch (NotSupportedException
| SystemException
| RollbackException
| HeuristicMixedException
| HeuristicRollbackException ex) {
ex.printStackTrace();
}
}
}
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.BeanManagementEjb;
@WebServlet("/bmt")
public class BeanManagementServlet extends HttpServlet {
@EJB
private BeanManagementEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
this.ejb.method(Boolean.valueOf(req.getParameter("rollback")));
}
}
http://localhost:8080/jpa/bmt?rollback=false
にアクセスする。
sample_table
を確認する。
http://localhost:8080/jpa/bmt?rollback=true
にアクセスする。
sample_table
を確認する。
-
@TransactionManagement
で EJB をアノテートして、TransactionManagementType.BEAN
を指定すると、トランザクションの管理を全て手動で行えるようになる。- この状態を、BMT (Bean Management Transaction) と呼ぶ。
- デフォルトのコンテナが管理している状態を CMT (Container Management Transaction) と呼ぶ。
- トランザクションの制御には
UserTransaction
を使用する。-
UserTransaction
のインスタンスは、@Resource
アノテーションで EJB にインジェクションできる。
-
ロック
楽観的ロック
エンティティ
package sample.javaee.jpa.entity;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
@Entity
@Table(name = "optimistick_lock_entity")
public class OptimisticLockEntity {
@Id
private Long id;
@Version
private Long version;
private String value;
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "OptimisticLockEntity{" + "id=" + id + ", version=" + version + ", value=" + value + '}';
}
}
データベース
CREATE TABLE `optimistick_lock_entity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` varchar(45) DEFAULT NULL,
`version` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
動作確認
package sample.javaee.jpa.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.OptimisticLockEntity;
@Stateless
public class OptimisticLockEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
public void update(String value) {
OptimisticLockEntity entity = this.em.find(OptimisticLockEntity.class, 1L);
entity.setValue(value);
}
}
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.OptimisticLockEjb;
@WebServlet("/optimistic-lock/*")
public class OptimisticLockServlet extends HttpServlet {
@EJB
private OptimisticLockEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
this.ejb.update(req.getParameter("value"));
}
}
http://localhost:8080/jpa/optimistic-lock?value=update
にアクセスして、データベースの状態を確認する。
- エンティティとテーブルに、更新回数を記録するための専用の項目を用意する(
version
)。- 数値型だけでなく、日付型も可能。
- そして、エンティティの当該フィールドを
@Version
でアノテートすることで、楽観的ロックを使用できるようになる。 -
@Version
でアノテートされたフィールドが存在すると、そのエンティティが更新されるときに JPA が自動で値をインクリメントするようになる。
バージョン番号が異なると例外がスローされる
package sample.javaee.jpa.ejb;
+ import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.OptimisticLockEntity;
@Stateless
public class OptimisticLockEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
+ @Asynchronous
- public void update(String value) {
+ public void update(String value, long time) {
OptimisticLockEntity entity = this.em.find(OptimisticLockEntity.class, 1L);
entity.setValue(value);
+
+ try {
+ Thread.sleep(time);
+ } catch (InterruptedException ex) {}
}
}
- EJB のメソッドを非同期処理にして、
Thread.sleep()
でトランザクションの完了を遅らせるようにしている。
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.OptimisticLockEjb;
@WebServlet("/optimistic-lock/*")
public class OptimisticLockServlet extends HttpServlet {
@EJB
private OptimisticLockEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
- this.ejb.update(req.getParameter("value"));
+ this.ejb.update(req.getParameter("value") + "_first", 100);
+ this.ejb.update(req.getParameter("value") + "_second", 10);
}
}
http://localhost:8080/jpa/optimistic-lock?value=test
にアクセスする。
Exception Description: The object [OptimisticLockEntity{id=1, version=2, value=test_first}] cannot be updated because it has changed or been deleted since it was last read.
Class> sample.javaee.jpa.entity.OptimisticLockEntity Primary Key> 1
(以下略)
データベースの状態。
- 1回目の
update()
がコミットされる前に2回目のupdate()
がコミットされる。 - このため、1回目の
update()
がコミットされる時点で、エンティティの持つversion
とデータベースのversion
に齟齬が生まれ、OptimisticLockException
がスローされる。
悲観的ロック
エンティティ
package sample.javaee.jpa.entity;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "pessimistic_lock_entity")
public class PessimisticLockEntity {
@Id
private Long id;
private String value;
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "PessimisticLockEntity{" + "id=" + id + ", value=" + value + '}';
}
}
データベース
CREATE TABLE `pessimistic_lock_entity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
動作確認
package sample.javaee.jpa.ejb;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.PessimisticLockEntity;
@Stateless
public class PessimisticLockEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
@Asynchronous
public void update(String value) {
System.out.println("start update(" + value + ")");
PessimisticLockEntity entity = this.em.find(PessimisticLockEntity.class, 1L, LockModeType.PESSIMISTIC_READ);
System.out.println("found entity : " + value);
entity.setValue(value);
System.out.println("end update(" + value + ")");
}
}
package sample.javaee.jpa.servlet;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.jpa.ejb.PessimisticLockEjb;
@WebServlet("/pessimistic-lock")
public class PessimisticLockServlet extends HttpServlet {
@EJB
private PessimisticLockEjb ejb;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
this.ejb.update(req.getParameter("value") + "_first");
this.ejb.update(req.getParameter("value") + "_second");
}
}
http://localhost:8080/jpa/pessimistic-lock?value=update
にアクセスする。
情報: start update(update_first)
情報: start update(update_second)
普通: SELECT ID, VALUE FROM pessimistic_lock_entity WHERE (ID = ?) FOR UPDATE
bind => [1]
情報: found entity : update_second
情報: end update(update_second)
普通: UPDATE pessimistic_lock_entity SET VALUE = ? WHERE (ID = ?)
bind => [update_second, 1]
普通: SELECT ID, VALUE FROM pessimistic_lock_entity WHERE (ID = ?) FOR UPDATE
bind => [1]
情報: found entity : update_first
情報: end update(update_first)
普通: UPDATE pessimistic_lock_entity SET VALUE = ? WHERE (ID = ?)
bind => [update_first, 1]
データベースの状態。
- SELECT に
FOR UPDATE
が追加され、悲観的ロックが実行されている(update_first
はupdate_second
の終了を待機している)。 -
EntityManager.find()
のときに、引数でLockModeType
を渡すことで、ロックを指定することができる。-
LockModeType.PESSIMISTIC_READ
は共有ロックで、LockModeType.PESSIMISTIC_WRITE
は排他ロック。 - でも、実際に試したら、どちらも
FOR UPDATE
(排他ロック)になった。。。 原因は不明。
-
-
LockModeType.PESSIMISTIC_FORCE_INCREMENT
を指定すると、排他ロックをとりつつ@Version
でアノテートしたフィールドを更新してくれる。- もし、同じエンティティに対して楽観的ロックと悲観的ロックの両方を使うことがある場合は、この方法で悲観的ロックを取得する必要があるのかなぁと思っている。
- 楽観的ロックのときは省略していたが、
LockModeType.OPTIMISTIC
を指定すれば明示的に楽観的ロックが取得できる。
任意のタイミングでロックを取得する
package sample.javaee.jpa.ejb;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import sample.javaee.jpa.entity.PessimisticLockEntity;
@Stateless
public class PessimisticLockEjb {
@PersistenceContext(unitName="SampleUnit")
private EntityManager em;
@Asynchronous
public void update(String value) {
System.out.println("start update(" + value + ")");
- PessimisticLockEntity entity = this.em.find(PessimisticLockEntity.class, 1L, LockModeType.PESSIMISTIC_READ);
+ PessimisticLockEntity entity = this.em.find(PessimisticLockEntity.class, 1L);
System.out.println("found entity : " + value);
+ this.em.lock(entity, LockModeType.PESSIMISTIC_READ);
+
+ System.out.println("lock entity : " + value);
entity.setValue(value);
System.out.println("end update(" + value + ")");
}
}
http://localhost:8080/jpa/pessimistic-lock?value=UPDATE
にアクセスする。
情報: start update(UPDATE_second)
情報: start update(UPDATE_first)
普通: SELECT ID, VALUE FROM pessimistic_lock_entity WHERE (ID = ?)
bind => [1]
普通: SELECT ID, VALUE FROM pessimistic_lock_entity WHERE (ID = ?)
bind => [1]
情報: found entity : UPDATE_second
情報: found entity : UPDATE_first
普通: SELECT ID, VALUE FROM pessimistic_lock_entity WHERE (ID = ?) FOR UPDATE
bind => [1]
普通: SELECT ID, VALUE FROM pessimistic_lock_entity WHERE (ID = ?) FOR UPDATE
bind => [1]
情報: lock entity : UPDATE_second
情報: end update(UPDATE_second)
普通: UPDATE pessimistic_lock_entity SET VALUE = ? WHERE (ID = ?)
bind => [UPDATE_second, 1]
情報: lock entity : UPDATE_first
情報: end update(UPDATE_first)
普通: UPDATE pessimistic_lock_entity SET VALUE = ? WHERE (ID = ?)
bind => [UPDATE_first, 1]
データベースの状態。
-
EntityManager#lock(Object, LockModeType)
を使えば、任意のタイミングでロックを取得することができる。
発行される SQL をログに出力させる
JPA の実装が実際にデータベースに発行している SQL を確認したくなるときがある(主にデバッグ時)。
GlassFish 4.1 は、 JPA の実装として EclipseLink を使用している。 EclipseLink で SQL をログに出力させるためには、 persistence.xml
で以下のように設定を記述する。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="SampleUnit">
<jta-data-source>jdbc/Local_MySQL_test</jta-data-source>
+ <properties>
+ <property name="eclipselink.logging.level" value="FINE" />
+ <property name="eclipselink.logging.parameters" value="true" />
+ </properties>
</persistence-unit>
</persistence>
-
<properties>
タグを追加し、eclipselink.logging.level
とeclipselink.logging.parameters
を設定している。 - 前者は SQL のログ出力で、後者はバインドパラメータの具体値の出力を指定している。
http://localhost:8080/jpa/hello?id=2
にアクセスすると、以下のようにコンソールにログが出力される。
普通: SELECT ID, VALUE FROM sample_table WHERE (ID = ?)
bind => [2]