環境構築
マッピングの話
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属性を追加して指定する(JTAorRESOURCE_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]






















