トランザクション その5
前回、複数Daoにまたがる場合のトランザクション処理をやってみましたが、あの設計だと複数人で開発した場合早晩破綻するのでもうちょっと工夫してみましょう。
登場人物は以下の3つ。
- 単純なDao。基本的にテーブルに付き1つのDaoが作られる。これには Transaction を付けない。
- Dao Factory インターフェイス。全部のDaoを CreateSqlObject 宣言する。
- トランザクション処理abstクラス。トランザクションを必要とする場合は、Factory Dao インターフェイス をimplementsして実際のトランザクション処理を記述する。規模が小さければこのファイルは1つ。規模が大きければ複数になる。
単純な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 }
いくつか注意点を
自前でバインドする
前回からの続きですが、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 }
いくつか注意点を
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 }