JavaEE使い方メモ(JPA その1 - 基本)

  • 77
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

環境構築
マッピングの話
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;

javaee-jpa.JPG

以下の 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');

javaee-jpa.JPG

データソースを GlassFish に登録する

こちら を参照。

JNDI 名は jdbc/Local_MySQL_test で登録しておく。

エンティティを作成する

Sample.java
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 上は、「構成ファイル」に表示される)。
  • 不要な記述などを削除して、整形する。
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>
  </persistence-unit>

</persistence>
  • このままだと war にしたときに persistence.xml が既定の場所(WEB-INF/classes/META-INF/)に配置されないので、プロジェクトの設定を修正する。
  • プロジェクトを右クリックして、 [プロパティ] を選択。
  • [カテゴリ] で "ソース" を選択。
  • [ソース・パッケージ・フォルダ] の [フォルダの追加] を選択。
  • src/conf を選択して [開く] をクリック。

javaee-jpa.JPG

  • これで、 persistence.xmlWEB-INF/classes/META-INF/ の下に配備されるようになる。

DBアクセス処理を実装する

HelloJpaServlet.java
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"));
    }
}
HelloJpaEjb.java
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!! にアクセスする。

GlassFishコンソール出力
情報: persist : Sample{id=null, value=HelloJPA!!}

sample_table の中を確認してみる。

javaee-jpa.JPG

http://localhost:8080/jpa/hello?id=4 にアクセスする。

GlassFishコンソール出力
情報: Sample{id=4, value=HelloJPA!!}

http://localhost:8080/jpa/hello/remove?id=4 にアクセスする。

GlassFishコンソール出力
情報: remove : Sample{id=4, value=HelloJPA!!}

sample_table の中を確認してみる。

javaee-jpa.JPG

説明

永続ユニット

persistence.xml
  <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 or RESOURCE_LOCAL)。
  • <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

エンティティ

Sample.java
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

HelloJpaEjb.java
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 にもインジェクションできる。
  • @PersistenceContextname 属性で、使用する永続ユニットの名前を指定する。
  • EntityManager には、データベースにアクセスするための CRUD 操作メソッドが定義されており、それを使ってエンティティの取得、登録、削除などを行うことができる。
    • EntityManager#find(Class, Object) で、キー情報を使ってエンティティを取得する。
    • EntityManager#persist(Object) で、指定したオブジェクトに対応するレコードをテーブルに登録する。
    • EntityManager#remove(Object) で、指定したオブジェクトに対応するレコードをテーブルから削除する。

レコードの更新

HelloJpaEjb.java
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);
+   }
}
HelloJpaServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: Sample{id=2, value=fuga}

http://localhost:8080/jpa/hello/update?id=2&value=UpdateValue にアクセスする。

GlassFishコンソール出力
情報: update : Sample{id=2, value=UpdateValue}

http://localhost:8080/jpa/hello?id=2 にアクセスする。

GlassFishコンソール出力
情報: Sample{id=2, value=UpdateValue}

JPA では、 DB のレコードを更新するために UPDATE 文を明示的に実行することはない。

JPA でのレコードの更新は、 find() で取得したオブジェクトのフィールドを書き換えるだけでいい。
トランザクションがコミットされるタイミングで、 EntityManager がデータベースに変更を反映してくれる。

たぶん、 SQL をガリガリ書く実装をしていた人が JPA をやり始めて、一番最初に面食らう仕様だと思う。

最初は、ちょっとフィールドを変更しただけで DB に反映されることに対する不安感や不信感があるかもしれない。
しかし、この仕組みがあると実装からデータベース(インフラストラクチャ)に関する処理を取り除くことができ、よりドメインモデルに集中することができる(はず)。

ただし、ドメインモデルに重きをおくことはパフォーマンスとのトレードオフな部分もある。
O/R マッパーに関するパフォーマンスの問題とその対策については、以下のスライドが分かりやすいです。

EntityManager によるオブジェクトの管理

JPA では、エンティティオブジェクトの状態が EntityManager によって管理・監視されている。

EntityManager がエンティティオブジェクトを監視していることで、前述のようにフィールドを書き換えただけでデータベースに変更を反映することができている。
逆にいうと、 EntityManager に管理されていないオブジェクトは、フィールドを変更してもデータベースに反映されることはない。

JPA では、エンティティオブジェクトが EntityManager に管理されているかどうか、というのを意識するのが重要になる。

新しいエンティティオブジェクトを作成する

EntityStateManagementEjb.java
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 の管理下に置く

EntityStateManagementEjb.java
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 に記録されているエンティティの情報を破棄する

EntityStateManagementEjb.java
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 の管理下から外す

EntityStateManagementEjb.java
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 と同期されず、レコードは登録されない。

手動でデータベースに変更を反映させる

EntityStateManagementEjb.java
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 状態に戻す

EntityStateManagementEjb.java
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("こちらの値は反映されない");
    }
}

javaee-jpa.JPG

  • EntityManager#merge(Object) メソッドを使うことで、 DETACHED 状態のエンティティオブジェクトを MANAGED 状態に戻すことができる。
  • NEW 状態のエンティティオブジェクトを渡すこともでき、その場合はレコードが追加される。
  • 引数で渡したエンティティオブジェクトは MANAGED 状態にならない。
    • 代わりに、戻り値のオブジェクトが MANAGED 状態になっている(こちらのオブジェクトに変更を加えると、 DB に反映される)。
    • 戻り値のオブジェクトは、引数で渡したオブジェクトのコピー。

MANAGED 状態のオブジェクトを DB の最新の状態で更新する

EntityStateManagementEjb.java
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 にアクセスする。

GlassFishコンソール出力
情報: 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 コンテナは自動でメソッドの前後にトランザクション境界を設ける。
つまり、メソッドの開始前にトランザクションが開始され、メソッドが終了するとトランザクションがコミットされる。

この動作は、アノテーションを使って細かく制御することができる。

アノテーションで調整する

デフォルトの動き

TransactionServlet.java
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();
        }
    }
}
TransactionEjb.java
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();
    }
}
DefaultTxEjb.java
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 にアクセスする。

GlassFishコンソール出力
情報: [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 のメソッドが実行された場合、同じトランザクションが引き継がれている。

常に新しいトランザクションを開始させる

RequiresNewTxEjb.java
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());
+   }
}
TransactionServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: [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 のトランザクションが終了したあとに再開される。

呼び出し元でトランザクションが開始されていれば、トランザクションを引き継ぐ

開始されていない場合は、トランザクションを開始しない

SupportsTxEjb.java
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());
    }
}
TransactionEjb.java(追加分のみ)
+   @EJB
+   private SupportsTxEjb supportsTxEjb;

+   public void supports() {
+       this.printTxId();
+       this.supportsTxEjb.method();
+   }
TransactionServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: [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 を指定すると、呼び出し元でトランザクションが開始されていれば、そのトランザクションを引き継ぐ。
  • 呼び出し元でトランザクションが開始されていない場合は、トランザクションを開始せずにメソッドを実行する。

開始されていない場合、例外をスローさせる

MandatoryTxEjb.java
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());
    }
}
TransactionEjb.java(追加分のみ)
+ @EJB
+ private MandatoryTxEjb mandatoryTxEjb;

+ public void mandatory() {
+    this.printTxId();
+    this.mandatoryTxEjb.method();
+ }
TransactionServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: [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 を指定すると、呼び出し元でトランザクションが開始していない場合、例外をスローさせることができる。

呼び出し元でトランザクションが開始されている場合、例外をスローさせる

NeverTxEjb.java
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());
    }
}
TransactionEjb.java(追加分のみ)
+   @EJB
+   private NeverTxEjb neverTxEjb;

+   public void never() {
+       this.printTxId();
+       this.neverTxEjb.method();
+   }
TransactionServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: [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 を指定すると、呼び出し元でトランザクションが開始されていると例外をスローさせることができる。

トランザクションを一切使用させないようにする

NotSupportedTxEjb.java
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());
    }
}
TransactionEjb.java(追加分のみ)
+   @EJB
+   private NotSupportedTxEjb notSupportedTxEjb;

+   public void notSupported() {
+       this.printTxId();
+       this.notSupportedTxEjb.method();
+   }
TransactionServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: [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 なし(中断)

ロールバック

例外によるロールバック

MyRollbackException.java
package sample.javaee.jpa.exception;

import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class MyRollbackException extends Exception {
}
ExceptionRollbackEjb.java
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);
    }
}
ExceptionRollbackServlet.java
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 にアクセスする。

GlassFishコンソール出力
重大: java.lang.Exception: test exception
   at sample.javaee.jpa.ejb.ExceptionRollbackEjb.throwException(ExceptionRollbackEjb.java:16)
(以下略)

sample_table を確認する。

javaee-jpa.JPG

レコードが登録されている。

http://localhost:8080/jpa/ex-rollback/runtime にアクセスする。

GlassFishコンソール出力
警告: 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 を確認する。

javaee-jpa.JPG

レコードは登録されていない。

http://localhost:8080/jpa/ex-rollback/my-exception にアクセスする。

GlassFishコンソール出力
重大: sample.javaee.jpa.exception.MyRollbackException
   at sample.javaee.jpa.ejb.ExceptionRollbackEjb.throwMyRollbackException(ExceptionRollbackEjb.java:27)
(以下略)

sample_table を確認する。

javaee-jpa.JPG

レコードは登録されていない。

  • Exception を継承した例外がスローされた場合、トランザクションはロールバックされない
    • @ApplicationException で例外クラスをアノテートし rollback=true を設定すると、ロールバックさせることができる。
  • RuntimeException を継承した例外がスローされた場合、トランザクションはロールバックされる。

手動ロールバック

ManualRollbackEjb.java
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();
    }
}
ManualRollbackServlet.java
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 を確認する。

javaee-jpa.JPG

  • SessionContext#setRollbackOnly() を実行することで、現在のトランザクションを必ずロールバックさせるように指定できる。
  • SessionContext は、 @Resource アノテーションを使って EJB にインジェクションする。

トランザクションを手動で管理する

BeanManagementEjb.java
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();
        }
    }
}
BeanManagementServlet.java
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 を確認する。

javaee-jpa.JPG

http://localhost:8080/jpa/bmt?rollback=true にアクセスする。

sample_table を確認する。

javaee-jpa.JPG

  • @TransactionManagement で EJB をアノテートして、 TransactionManagementType.BEAN を指定すると、トランザクションの管理を全て手動で行えるようになる。
    • この状態を、BMT (Bean Management Transaction) と呼ぶ。
    • デフォルトのコンテナが管理している状態を CMT (Container Management Transaction) と呼ぶ。
  • トランザクションの制御には UserTransaction を使用する。
    • UserTransaction のインスタンスは、 @Resource アノテーションで EJB にインジェクションできる。

ロック

楽観的ロック

エンティティ

javaee-jpa.JPG

OptimisticLockEntity.java
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 + '}';
    }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

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;

動作確認

OptimisticLockEjb.java
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);
    }
}
OptimisticLockServlet.java
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 にアクセスして、データベースの状態を確認する。

javaee-jpa.JPG

  • エンティティとテーブルに、更新回数を記録するための専用の項目を用意する(version)。
    • 数値型だけでなく、日付型も可能。
  • そして、エンティティの当該フィールドを @Version でアノテートすることで、楽観的ロックを使用できるようになる。
  • @Version でアノテートされたフィールドが存在すると、そのエンティティが更新されるときに JPA が自動で値をインクリメントするようになる。

バージョン番号が異なると例外がスローされる

OptimisticLockEjb.java
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() でトランザクションの完了を遅らせるようにしている。
OptimisticLockServlet.java
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 にアクセスする。

GlassFishコンソール出力
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
(以下略)

データベースの状態。

javaee-jpa.JPG

  • 1回目の update() がコミットされる前に2回目の update() がコミットされる。
  • このため、1回目の update() がコミットされる時点で、エンティティの持つ version とデータベースの version に齟齬が生まれ、 OptimisticLockException がスローされる。

悲観的ロック

エンティティ

javaee-jpa.JPG

PessimisticLockEntity.java
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 + '}';
    }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

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;

動作確認

PessimisticLockEjb.java
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 + ")");
    }
}
PessimisticLockServlet.java
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 にアクセスする。

GlassFishコンソール出力
情報: 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]

データベースの状態。

javaee-jpa.JPG

  • SELECT に FOR UPDATE が追加され、悲観的ロックが実行されている(update_firstupdate_second の終了を待機している)。
  • EntityManager.find() のときに、引数で LockModeType を渡すことで、ロックを指定することができる。
    • LockModeType.PESSIMISTIC_READ は共有ロックで、 LockModeType.PESSIMISTIC_WRITE は排他ロック。
    • でも、実際に試したら、どちらも FOR UPDATE (排他ロック)になった。。。 原因は不明。
  • LockModeType.PESSIMISTIC_FORCE_INCREMENT を指定すると、排他ロックをとりつつ @Version でアノテートしたフィールドを更新してくれる。
    • もし、同じエンティティに対して楽観的ロックと悲観的ロックの両方を使うことがある場合は、この方法で悲観的ロックを取得する必要があるのかなぁと思っている。
  • 楽観的ロックのときは省略していたが、 LockModeType.OPTIMISTIC を指定すれば明示的に楽観的ロックが取得できる。

任意のタイミングでロックを取得する

PessimisticLockEjb.java
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 にアクセスする。

GlassFishコンソール出力
情報: 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]

データベースの状態。

javaee-jpa.JPG

  • EntityManager#lock(Object, LockModeType) を使えば、任意のタイミングでロックを取得することができる。

発行される SQL をログに出力させる

JPA の実装が実際にデータベースに発行している SQL を確認したくなるときがある(主にデバッグ時)。

GlassFish 4.1 は、 JPA の実装として EclipseLink を使用している。 EclipseLink で SQL をログに出力させるためには、 persistence.xml で以下のように設定を記述する。

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.leveleclipselink.logging.parameters を設定している。
  • 前者は SQL のログ出力で、後者はバインドパラメータの具体値の出力を指定している。

http://localhost:8080/jpa/hello?id=2 にアクセスすると、以下のようにコンソールにログが出力される。

普通: SELECT ID, VALUE FROM sample_table WHERE (ID = ?)
   bind => [2]

参考