Google Guice で JPA のトランザクションを管理する

トランザクション管理って面倒ですよね。JPA を使うときを考えると、まず EntityManagerFactory を作って、EntityManager を取得して、EntityTransaction を取得して、開始して、コミットして、例外が上がったらロールバックして…。うーん、継承などで処理を親クラスにまとめるにしても、どうもイマイチな感じが拭えません。

EJB ならコンテナが管理してくれる?いや EJB はちょっと…。Spring の AOP はどうか?いや大量の XML に悩まされるのはちょっと…。そんな好き嫌いの激しいぼく的には、シンプルな DIコンテナ Google GuiceAOP を使ってトランザクション管理ができるとうれしいなー、と思いました。最初は自分でインターセプターを書こうと意気込んでいたんですが、何とモジュールが既にあったのでこれ幸いと使ってみました。

ソース

まずは Google Guice の設定に使うモジュールです。

package study.jpa.guice.transactional;

import com.google.inject.AbstractModule;
import com.google.inject.persist.jpa.JpaPersistModule;

public class JpaPersistDaoModule extends AbstractModule {

	@Override
	protected void configure() {
		JpaPersistModule jpaPersistModule = new JpaPersistModule("example-persist-unit");
		this.install(jpaPersistModule);
		this.bind(ProductDao.class).to(ProductDaoImpl.class);
	}
	
}

JpaPersistModule クラスがトランザクションの管理に必要な処理を織り込んでくれるモジュールです。コンストラクタには JPA の永続ユニット名を指定します。JpaPersistDaoModule クラスでは JpaPersistModule のインスタンスをインストールした上で、エンティティの永続化に使う DAO を登録します。

続いて Main です。

package study.jpa.guice.transactional;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {

	public static void main(String[] args) throws Exception {
		Injector injector = Guice.createInjector(new JpaPersistDaoModule());
		injector.getInstance(JpaPersistInitializer.class);
		ProductDao dao = injector.getInstance(ProductDaoImpl.class);
		dao.add("Ninjin", 98);
		dao.add("Daikon", 168);
	}
}

先ほど作ったモジュールを使って Injector クラスのインスタンスを生成します。次に JpaPersistInitializer クラスのインスタンスGuice 経由で取得していますが、これは必要な初期化を行うクラスです。初期化が完了した後に Guice 経由で DAO を取得して、データを追加しています。

先ほど登場した JpaPersistInitializer クラスです。

package study.jpa.guice.transactional;

import com.google.inject.Inject;
import com.google.inject.persist.PersistService;

public class JpaPersistInitializer {

	@Inject
	public JpaPersistInitializer(PersistService service) {
		service.start();
	}
}

このクラスではコンストラクタインジェクションで PersistService のインスタンスを受け取った上で start メソッドを呼んでいます。JpaPersistModule クラスを使ってトランザクション管理をする場合には、あらかじめ PersistService#start() を呼んでおく必要があります。

ProductDao クラスは単なるインタフェースです。

package study.jpa.guice.transactional;

public interface ProductDao {
	public Integer add(String name, Integer price);
}

ProductDaoImpl クラスに嬉しさが詰まっています。

package study.jpa.guice.transactional;

import com.google.inject.persist.Transactional;
import javax.inject.Inject;
import javax.persistence.EntityManager;

public class ProductDaoImpl implements ProductDao {

	@Inject
	private EntityManager em;

	@Transactional
	@Override
	public Integer add(String name, Integer price) {
		Product product = new Product();
		product.setName(name);
		product.setPrice(price);
		em.persist(product);
		return product.getId();
	}
}

EntityManager のインスタンスGuice が注入してくれます。更に、トランザクションについては、管理してほしいメソッドを @Transactional アノテーションで修飾するだけです。うーん、なんてシンプル!

あとは至って普通の JPA を使ったアプリケーションです。永続化するエンティティの Product クラス。

package study.jpa.guice.transactional;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Product implements Serializable {

	@Id
	@GeneratedValue
	protected Integer id;
	
	@Column(nullable = false, unique = true)
	protected String name;
	
	@Column(nullable = false)
	protected Integer price;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getPrice() {
		return price;
	}

	public void setPrice(Integer price) {
		this.price = price;
	}

}

永続ユニット (persistence.xml) です。JPA の実装には Hibernate を、DB には MySQL を使いました。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
	<persistence-unit name="example-persist-unit" transaction-type="RESOURCE_LOCAL">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<properties>
			<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/test"/>
			<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
			<property name="javax.persistence.jdbc.user" value="****"/>
			<property name="javax.persistence.jdbc.password" value="****"/>
			<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
			<property name="hibernate.hbm2ddl.auto" value="update" />
			<property name="hibernate.show_sql" value="true" />
			<property name="hibernate.format_sql" value="true" />
		</properties>
	</persistence-unit>
</persistence>

プロジェクトの管理は Maven です。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>study.guice.jpa.transactional</groupId>
	<artifactId>study-guice-jpa-transactional</artifactId>
	<version>1.0-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>study-guice-jpa-transactional</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.2.1</version>
				<configuration>
					<mainClass>study.jpa.guice.transactional.Main</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<!-- compile -->
		<dependency>
			<groupId>com.google.inject</groupId>
			<artifactId>guice</artifactId>
			<version>3.0</version>
		</dependency>
		<dependency>
			<groupId>com.google.inject.extensions</groupId>
			<artifactId>guice-persist</artifactId>
			<version>3.0</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>4.0.1.Final</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.18</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-core</artifactId>
			<version>1.0.0</version>
		</dependency>
	</dependencies>
</project>

実行

Main の実行結果です。MySQL は起動しておきます。

$ mvn clean compile exec:java
(…省略…)
Hibernate: 
    insert 
    into
        Product
        (name, price) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Product
        (name, price) 
    values
        (?, ?)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.952s
[INFO] Finished at: Thu Feb 09 23:09:07 JST 2012
[INFO] Final Memory: 11M/81M
[INFO] ------------------------------------------------------------------------

上手いこと SQL が発行されたみたいですね。

DB の確認

念のため MySQL で直接確認しておきます。

mysql> select * from Product;
+----+--------+-------+
| id | name   | price |
+----+--------+-------+
|  1 | Ninjin |    98 |
|  2 | Daikon |   168 |
+----+--------+-------+
2 rows in set (0.00 sec)

意図した通りに行が追加されていますね。

ロールバックの確認

例外が上がったときに正しくロールバックされるか確認します。確認方法としては、永続化に使ったエンティティの Product クラスの name フィールドに unique 制約を付けておいたので、もう一度同じように Maven を実行するだけです。もう一度実行すると、同じ name を持った行が追加されようとすることで PersistenceException が上がって失敗します。

ロールバックの動作は MySQL のログで確認します。ログの出し方はググってください。

                    3 Query     SET autocommit=0
                    3 Query     insert into Product (name, price) values ('Ninjin', 98)
                    3 Query     rollback

トランザクションの開始、行の挿入、ロールバックの挙動が確認できます。

まとめ

面倒だった JPAトランザクション管理も Google Guice を使って簡単に分かりやすくできました。