トランザクション その5

前回、複数Daoにまたがる場合のトランザクション処理をやってみましたが、あの設計だと複数人で開発した場合早晩破綻するのでもうちょっと工夫してみましょう。
登場人物は以下の3つ。

単純なDaoクラス

Table001Dao.java
public interface Table001Dao {
	@SqlUpdate("insert into table001 (id, name) values (:id, :name)")
	public abstract int insert(@Bind("id") int id, @Bind("name") String name);

	@SqlQuery("select count(*) from table001")
	public abstract int findCount();
}
Table002Dao.java
public interface Table002Dao {
	@SqlUpdate("insert into table002 (id, mail) values (:id, :mail)")
	public abstract int insert(@Bind("id") int id, @Bind("mail") String mail);

	@SqlQuery("select count(*) from table002")
	public abstract int findCount();
}

Dao Factory インターフェイス

DaoFactory.java
public interface DaoFactory {
	@CreateSqlObject
	Table001Dao createTable001Dao();

	@CreateSqlObject
	Table002Dao createTable002Dao();
}

トランザクション処理abstクラス

TxDao.java
public abstract class TxDao implements DaoFactory
{
	@Transaction
	public void txSuccessTest()
	{
		Table001Dao dao1 = createTable001Dao();
		Table002Dao dao2 = createTable002Dao();

		dao1.insert(1, "name1");
		dao1.insert(2, "name2");
		dao1.insert(3, "name3");

		dao2.insert(1, "mail1@example.jp");
		dao2.insert(2, "mail2@example.jp");
		dao2.insert(3, "mail3@example.jp");
	}

	@Transaction
	public void txErrorTest()
	{
		Table001Dao dao1 = createTable001Dao();
		Table002Dao dao2 = createTable002Dao();

		dao1.insert(4, "name4");
		dao1.insert(5, "name5");
		dao1.insert(6, "name6");

		dao2.insert(4, "mail4@example.jp");
		dao2.insert(4, "mail5@example.jp"); // エラー
	}
}
Sample020.java
public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");

	TxDao dao = dbi.onDemand(TxDao.class);
	Table001Dao dao1 = dbi.onDemand(Table001Dao.class);
	Table002Dao dao2 = dbi.onDemand(Table002Dao.class);

	/*
	 * commit test
	 */
	System.out.println("commit before table001 rows count = " + dao1.findCount()); // 0
	System.out.println("commit before table002 rows count = " + dao2.findCount()); // 0
	try {
		dao.txSuccessTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("commit after table001 rows count = " + dao1.findCount()); // 3
	System.out.println("commit after table002 rows count = " + dao2.findCount()); // 3

	/*
	 * rollback test
	 */
	System.out.println("rollback before table001 rows count = " + dao1.findCount()); // 3
	System.out.println("rollback before table002 rows count = " + dao2.findCount()); // 3
	try {
		dao.txErrorTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("rollback after table001 rows count = " + dao1.findCount()); // 3
	System.out.println("rollback after table002 rows count = " + dao2.findCount()); // 3
}

トランザクション その4

今度は、複数Daoにまたがる場合のトランザクション処理をやってみましょう

Dao013.java
public interface Dao013 {
	@SqlUpdate("insert into table001 (id, name) values (:id, :name)")
	int insert(@Bind("id") int id, @Bind("name") String name);
}
Dao014.java
public abstract class  Dao014 {
	@SqlUpdate("CREATE TABLE IF NOT EXISTS table002(id integer NOT NULL, mail character varying(100), PRIMARY KEY (id))")
	public abstract void createTable();

	@SqlUpdate("insert into table002 (id, mail) values (:id, :mail)")
	public abstract int insert(@Bind("id") int id, @Bind("mail") String name);

	@SqlQuery("select count(*) from table001")
	public abstract int findCountTable001();

	@SqlQuery("select count(*) from table002")
	public abstract int findCountTable002();

	@CreateSqlObject
	public abstract Dao013 createSqlObject();

	@Transaction
	public void txSuccessTest()
	{
		insert(1, "mail1@example.jp");
		insert(2, "mail2@example.jp");
		insert(3, "mail3@example.jp");

		Dao013 dao013 = createSqlObject();
		dao013.insert(1, "name1");
		dao013.insert(2, "name2");
		dao013.insert(3, "name3");
	}

	@Transaction
	public void txErrorTest()
	{
		insert(4, "mail4@example.jp");
		insert(5, "mail5@example.jp");
		insert(6, "mail6@example.jp");

		Dao013 dao013 = createSqlObject();
		dao013.insert(4, "name4");
		dao013.insert(5, "name5");
		dao013.insert(5, "name6"); // エラー
	}
}
Sample019.java
public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");
	Dao014 dao = dbi.onDemand(Dao014.class);
	dao.createTable();

	/*
	 * commit test
	 */
	System.out.println("commit before table001 rows count = " + dao.findCountTable001()); // 0
	System.out.println("commit before table002 rows count = " + dao.findCountTable002()); // 0
	try {
		dao.txSuccessTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("commit after table001 rows count = " + dao.findCountTable001()); // 3
	System.out.println("commit after table002 rows count = " + dao.findCountTable002()); // 3

	/*
	 * rollback test
	 */
	System.out.println("rollback before table001 rows count = " + dao.findCountTable001()); // 3
	System.out.println("rollback before table002 rows count = " + dao.findCountTable002()); // 3
	try {
		dao.txErrorTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("rollback after table001 rows count = " + dao.findCountTable001()); // 3
	System.out.println("rollback after table002 rows count = " + dao.findCountTable002()); // 3
}

いくつか注意点を

  • メインのDaoは「Dao014」の方で、Dao014から、Dao013を呼び出しています
  • その際、CreateSqlObject アノテーションを付けたメソッドを定義する必要があります(他と同じように中身は空でJDBIが勝手にインジェクションしてくれます)
  • 後は Transaction アノテーション内で普通にDaoで定義されたメソッドを呼び出していけばOK

自前でバインドする

前回からの続きですが、ObjectAPI形式で引数を自前でバインドする方法です

Dao012.java
public interface Dao012 {
	@SqlUpdate("insert into table001 (id, name) values (:id, :name)")
	void insert(@BindMap Map<String,Object> map);

	@SqlQuery("select name from table001 where id=:id")
	String findNameById(@Bind("id") int id);
}
Sample018.java
@BindingAnnotation(BindMap.MapBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public static @interface BindMap
{
	public static class MapBinderFactory implements BinderFactory
	{
		@Override
		public Binder<BindMap, Map<String,Object>> build(Annotation annotation)
		{
			return new Binder<BindMap, Map<String,Object>>()
			{
				public void bind(SQLStatement<?> q, BindMap bind, Map<String,Object> arg)
				{
					for( Map.Entry<String, Object> pair : arg.entrySet() )
					{
						q.bind(pair.getKey(), pair.getValue());
					}
				}
			};
		}
	}
}

public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");
	Dao012 dao = dbi.onDemand(Dao012.class);

	Map<String,Object> map = new HashMap<>();
	map.put("id", 2002);
	map.put("name", "name2001");
	map.put("something_param", ""); // 余分なパラメータ

	dao.insert(map);
	System.out.println("id=2001 => " + dao.findNameById(2001)); // name2001
}

いくつか注意点を

  • @Retention(RetentionPolicy.RUNTIME) アノテーションは必須です。JDBIは実行時にアノテーションを探す仕組みになってますので
  • バインドさせる型は「Map」としないとダメで、「Map」だと、id に数値をセットしようとしてエラーになります。
  • たとえ余分なパラメータ(something_param)をバインドしても怒られないようです
  • このサンプルで作成したMap型のバインドは結構よく使うので便利です(自画自賛w)。というかデフォルトで用意してくれててもいいのになぁ

Beanをバインドした時に接頭辞を付ける

ObjectAPI形式の場合、引数にBeanがいくつもあった場合、同じフィールド名がかぶってしまう可能性があるので、そんな時にどのBeanがどのパラメータにバインドさせるかちゃんと指定できるようします

Dao011.java
public interface Dao011 {
	@SqlUpdate("insert into table001 (id, name) values (:b1.id, :b2.name)")
	void insert(
		@BindBean("b1") Sample017.Bean bean1,
		@BindBean("b2") Sample017.Bean bean2
	);

	@SqlQuery("select name from table001 where id=:id")
	String findNameById(@Bind("id") int id);
}
Sample017.java
public static class Bean
{
	public Bean(int id, String name) {
		this.id = id;
		this.name = name;
	}

	private int id;
	private String name;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");
	Dao011 dao = dbi.onDemand(Dao011.class);

	Bean bean1 = new Bean(1001, "name1");
	Bean bean2 = new Bean(1002, "name2");
	dao.insert(bean1, bean2);

	System.out.println("id=1001 => " + dao.findNameById(1001)); // name2
	System.out.println("id=1002 => " + dao.findNameById(1002)); // null
}

Dao011.insert のSQL文と引数に注目です

トランザクション その3

次はDaoに@Transactionアノテーションを付けてトランザクション制御してみます

Dao010.java
public abstract class Dao010 {
	@SqlUpdate("insert into table001 (id, name) values (:id, :name)")
	public abstract int insert(@Bind("id") int id, @Bind("name") String name);

	@SqlQuery("select count(*) from table001")
	public abstract int findCount();

	@Transaction
	public void txSuccessTest()
	{
		insert(1, "name1");
		insert(2, "name2");
		insert(3, "name3");
	}

	@Transaction
	public void txErrorTest()
	{
		insert(4, "name4");
		insert(5, "name5");
		insert(5, "name6"); // ここでエラー
	}

	public void noTxSuccessTest()
	{
		insert(11, "name11");
		insert(12, "name12");
		insert(13, "name13");
	}

	public void noTxErrorTest()
	{
		insert(14, "name14");
		insert(15, "name15");
		insert(15, "name16"); // ここでエラー
	}
}
Sample016.java
public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");
	Dao010 dao = dbi.onDemand(Dao010.class);

	// =====================================================================
	// @Transaction アノテーション有り
	// =====================================================================

	/*
	 * 成功すれば勝手にcommitしてくれる
	 */
	System.out.println("tx success before rows count = " + dao.findCount()); // 0
	try {
		dao.txSuccessTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("tx success after rows count = " + dao.findCount()); // 3

	/*
	 * 失敗すれば勝手にrollbackしてくれる
	 */
	System.out.println("tx error before rows count = " + dao.findCount()); // 3
	try {
		dao.txErrorTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("tx error rows count = " + dao.findCount()); // 3

	// =====================================================================
	// @Transaction アノテーション無し
	// =====================================================================

	/*
	 * 成功すれば当然auto-commit
	 */
	System.out.println("non-tx success before rows count = " + dao.findCount()); // 3
	try {
		dao.noTxSuccessTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("non-tx success after rows count = " + dao.findCount()); // 6

	/*
	 * 失敗しても途中までcommitしてしまう
	 */
	System.out.println("non-tx error before rows count = " + dao.findCount()); // 6
	try {
		dao.noTxErrorTest();
	} catch(Throwable e) {
		e.printStackTrace();
	}
	System.out.println("non-tx error after rows count = " + dao.findCount()); // 8
}

トランザクション その2

お次はバカ正直に自分でトランザクションを制御します

Sample015.java
public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");
	Handle h = dbi.open();
	System.out.println("TransactionIsolationLevel = " + h.getTransactionIsolationLevel()); // ※1

	Integer count = 0;

	/*
	 * commit test
	 */
	count = h.createQuery("select count(*) from table001").map(IntegerMapper.FIRST).first();
	System.out.println("before rows count = " + count);

	try {
		h.begin();

		h.insert("insert into table001(id, name) values(?, ?)", 1, "name1");
		h.insert("insert into table001(id, name) values(?, ?)", 2, "name2");
		h.insert("insert into table001(id, name) values(?, ?)", 3, "name3");

		h.commit();
	} catch(Throwable e) {
		h.rollback();
		e.printStackTrace();
	}

	count = h.createQuery("select count(*) from table001").map(IntegerMapper.FIRST).first();
	System.out.println("after rows count = " + count);

	/*
	 * rollback test
	 */
	count = h.createQuery("select count(*) from table001").map(IntegerMapper.FIRST).first();
	System.out.println("before rows count = " + count);

	try {
		h.begin();

		h.insert("insert into table001(id, name) values(?, ?)", 4, "name4");
		h.insert("insert into table001(id, name) values(?, ?)", 5, "name5");
		h.insert("insert into table001(id, name) values(?, ?)", 5, "name6");

		h.commit();
	} catch(Throwable e) {
		h.rollback();
		e.printStackTrace();
	}

	count = h.createQuery("select count(*) from table001").map(IntegerMapper.FIRST).first();
	System.out.println("after rows count = " + count);
}

※1 でトランザクション分離レベルを出力しています。PostgreSQLだと READ_COMMITTED がデフォルトみたいです。もちろん setTransactionIsolation メソッドで他の分離レベルに変更可能です。

トランザクション その1

実は SqlBatch アノテーションを付ければ勝手にトランザクション処理をしてくれます

Dao008.java
public interface Dao008 {
	@SqlBatch("insert into table001 (id, name) values (:id, :name)")
	int[] insertBatch(@Bind("id") List<Integer> ids, @Bind("name") List<String> names);

	@SqlQuery("select count(*) from table001")
	int findCount();
}
Sample014.java
public static void main(String[] args) {
	String url = "jdbc:postgresql://192.168.52.128/jdbi";
	DBI dbi = new DBI(url, "jdbi_user", "jdbi_pass");
	Dao008 dao008 = dbi.onDemand(Dao008.class);

	/*
	 * 成功すれば勝手にcommitしてくれる
	 */
	List<Integer> ids = Arrays.asList(new Integer[]{1,2,3});
	List<String> names = Arrays.asList(new String[]{"name1", "name2", "name3"});

	System.out.println("before rows count = " + dao008.findCount()); // 0

	try {
		int[] result = dao008.insertBatch(ids, names);
		System.out.println("result.length = " + result.length); // 3
	} catch(Throwable e) {
		e.printStackTrace();
	}

	System.out.println("after rows count = " + dao008.findCount()); // 3

	/*
	 * 失敗すれば勝手にrollbackしてくれる
	 */
	ids = Arrays.asList(new Integer[]{4,5,5});
	names = Arrays.asList(new String[]{"name4", "name5", "name6"});

	System.out.println("before rows count = " + dao008.findCount()); // 3

	try {
		int[] result = dao008.insertBatch(ids, names);
		System.out.println("result.length = " + result.length); // 例外飛ぶのでここにはこない
	} catch(Throwable e) {
		e.printStackTrace();
	}

	System.out.println("after rows count = " + dao008.findCount()); // 3
}