[Java][Spring Boot] @Transactionalでトランザクション処理してロールバックする。

2016.08.11

はじめに

データベースに対して日常的に行っている処理で、複数のクエリを実行し、どこかでエラーが発生したらなかった事にしたいという事が有ります。
トランザクションを使ってロールバックするという事ですね。
これはSpringの@Transactionalをアノテートする事で実現できるという事で試してみました。

環境

Mac OSX 10.10.5 Yosemite
Java 1.8.0_91
Spring Boot 1.3.7
PostgreSQL 9.5.1
Eclipse Mars

テーブル

CREATE TABLE public.fruit (
     id VARCHAR(2) NOT NULL,
     name VARCHAR(10),
     price integer,
     PRIMARY KEY(id)
 );

INSERT INTO fruit VALUES
   ('1','apple',300),
   ('2','orange',200),
   ('3','banana',100),
   ('4','cherry',50),
   ('5','pineapple',500),
   ('6','melon',800),
   ('7','watermelon', 600),
   ('8','strawberry',450);
postgres=# select * from fruit ;
 id |    name    | price 
----+------------+-------
 1  | apple      |   300
 2  | orange     |   200
 3  | banana     |   100
 4  | cherry     |    50
 5  | pineapple  |   500
 6  | melon      |   800
 7  | watermelon |   600
 8  | strawberry |   450
(8 rows)

コード

プロパティファイルと依存関係は割愛します。

エンティティ

package com.transaction.sql;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;
import lombok.ToString;

@Entity
@Data
@Table(name="fruit")
public class Fruit {

	@Id
	private String id;
	private String name;
	private Integer price;

}

lombokの@Dataを使用し、ToString、Getter、Setterを設定。

DAOインターフェース

package com.transaction.sql;

import java.io.Serializable;

public interface DaoInterface <T> extends Serializable {

	void getAll();
	void update(String setColumn, String val, String whereColumn, String num1, String num2);
	void delete(String attributeName, String value);
	void insert(String column, String val1, Integer val2);

}

名前通りの処理を行うメソッドを用意。

DAO

package com.transaction.sql;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.CriteriaUpdate;
import javax.persistence.criteria.Root;

import org.springframework.transaction.annotation.Transactional;


public class DaoObject implements DaoInterface<Fruit> {

	private EntityManager entityManager;
	private CriteriaBuilder builder;

	public DaoObject(EntityManager entityManager) {
		super();
		this.entityManager = entityManager;
		this.builder = this.entityManager.getCriteriaBuilder();
	}

	@Override
	public void getAll() throws RuntimeException {
		CriteriaQuery<Fruit> query = builder.createQuery(Fruit.class);
		Root<Fruit> root = query.from(Fruit.class);
		query.select(root);
		List<Fruit> list = entityManager.createQuery(query).getResultList();
		System.out.println(list.toString());
	}

	@Override
	public void update(String col1, String val, String col2, String start, String end) throws RuntimeException {
		CriteriaUpdate<Fruit> update = builder.createCriteriaUpdate(Fruit.class);
		Root root = update.from(Fruit.class);
		update.set(col1, val); 
		update.where(builder.between(root.get(col2), start, end));
		int result = this.entityManager.createQuery(update).executeUpdate();
		System.out.println("UPDATE = " + result);
	}

	@Override
	public void delete(String column, String val) throws RuntimeException {
		CriteriaDelete<Fruit> delete = builder.createCriteriaDelete(Fruit.class);
		Root root = delete.from(Fruit.class);
		delete.where(builder.equal(root.get(column), val));
		Query query = entityManager.createQuery(delete);
		query.executeUpdate();
	}

	@Override
	public void insert(String column, String val1, Integer val2) throws RuntimeException {
			Fruit fruit = new Fruit();
			fruit.setId(column);
			fruit.setName(val1);
			fruit.setPrice(val2);
			entityManager.persist(fruit);
	}
	
}

27〜34行目、getAll()。全件取得するメソッド。確認用として一応用意しました。
発行されるネイティブクエリは下記。
SELECT * FROM テーブル名;

36〜44行目、update()。対象レコードを更新します。
CriteriaUpdateを使用する事以外はgetAll()とほぼ同じですね。
発行されるネイティブクエリは下記。
UPDATE テーブル
SET カラム1 = 値
WHERE カラム2 BETWEEN 値1 AND 値2;

46〜53行目、delete()。対象レコードを削除します。
こちらはCriteriaDeleteを使用します。
発行されるネイティブクエリは下記。
DELETE テーブル名 WHERE カラム名 = 値;

55〜62行目、insert()。レコードを挿入します。
CriteriaではCriteriaInsertは用意されていない様なので、エンティティに値をセットしたものを作成してEntityManagerに投げています。
発行されるネイティブクエリは下記。
INSERT INTO テーブル名 (id, name, price) VALUES (値1, 値2, 値3);

トランザクションするクラス

package com.transaction.sql;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class TransactionTest {

	@PersistenceContext
	private EntityManager entityManager;
	private DaoObject dao;

	@PostConstruct
	public void init() {
		dao = new DaoObject(entityManager);
	}

	@Transactional
	public void execute() {
		try {

			dao.update("name", "AAA", "id", "3", "5");
			dao.delete("id", "5");
			dao.insert("9", "blossom", 1000);

			//throw new RuntimeException();

		} catch (RuntimeException e) {
			e.printStackTrace();
		}
	}

}

17〜20行目、初期化処理。

22〜35行目、クエリを実行するメソッド。

22行目、@Transactionを付けているだけです。
ただし、ロールバックを有効にするには、このメソッドで例外を投げる必要が有ります。
メソッド内のメソッドで例外をtry-catchしても無効となるので注意。
詳細は下部の「ひっかかったところ」で紹介します。

30行目のコメントアウトを外して実行するとRuntimeExceptionが投げられ、ロールバックしてなかった事にしてくれます。

起動クラス

package com.transaction.sql;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class TransactionApplication {

	public static void main(String[] args) {
		try (ConfigurableApplicationContext cac = SpringApplication.run(TransactionApplication.class, args)) {
			TransactionTest app = cac.getBean(TransactionTest.class);
			app.execute();
		};
	}

}

DIコンテナで管理されているTransactionTestクラスのBeanを取得してexecute()を実行しているだけです。

ひっかかったところ

@Transactionalをアノテートすると対象のメソッドで発行したクエリのトランザクション処理を行ってくれますが、ロールバックを実現するには必ずアノテートしたメソッドでExceptionを投げる必要が有る様です。
最初、アノテートしたからなんでもトランザクションで良しなにしてくれるんだろうと思って下記の様なコードを書いていましたが、ロールバックは実行されないのでした。

ダメなコード例

@Override
public void update(String col1, String val, String col2, String start, String end) {
	try {

		CriteriaUpdate<Fruit> update = builder.createCriteriaUpdate(Fruit.class);
		Root root = update.from(Fruit.class);
		update.set(col1, val); 
		update.where(builder.between(root.get(col2), start, end));
		this.entityManager.createQuery(update).executeUpdate();
		
	} catch (RuntimeException e) {
	}
}
@Transactional
public void execute() {
	dao.update("name","AAA","id" ,"1","2");
	dao.update("name","BBB","ids","1","2");
}

2行目、このUPDATEは成功します。
3行目、こっちは失敗します。where句のカラム名を設定する箇所で"ids"という存在しないカラム指定しているためです。
しかし2行目のUPDATEがRollbackされずにテーブルを更新してしまいます。

さいごに

フレームワークを使用しなくてもロールバックは実現できますが、アノテーションだけで済むとだいぶすっきりしますね。