前提・実現したいこと
Javaでwebサービスを作っています。
DBはPostgreSQL9.6を使用しています。
少し古めのサービスでMyBatisではなく、iBATISにて動的なSQLを作成をしています。
・実現したいこと
PostgreSQLのカーソルを使用して、大量にあるレコードを少しずつ取得しながらUPDATEをしたい。
既存実装から、カーソルを宣言するトランザクションと、実際にUPDATE処理を行うトランザクションとで分けて実装する必要があった。
そこでここに書いてあるとおりにカーソルを宣言する際にCURSOR WITH HOLD FOR SELECT
とし、宣言したトランザクション外でもカーソルが使えるようにしました。
しかし、別のトランザクションでカーソルを使おうとしても、宣言したカーソルは存在しないとエラーが発生し、UPDATE処理ができなかった。
どうすればカーソルをほかのトランザクションでも使えるようになるのか、また、他に何か気をつけなればならないことがあるのであればご教授お願いいたします。
発生している問題・エラーメッセージ
下記のソースコードにてトランザクションを開始するか終了するかの宣言で、カーソルが存在しなくなる。
コミットする分にはカーソルは消えなかった。
ちなみにカーソルが存在するかの確認はSELECT * FROM pg_cursors;
を使い確認していた。
該当のソースコード
簡略化してるので抜けがあるかもしれません。
複数のトランザクションで処理
Java
1public class RecordEdit() { 2 // DBにアクセスするためのもの 3 final SqlMapClient sqlMapClient = baseDao.getSqlMapClient(); 4 // フェッチしてくる数を設定(1000件) 5 final int RECS_AT_ONCE = 1000; 6 7 // レコードIDを格納するリスト 8 Linkedlist<Long> recordIdList; 9 // カーソル名(本来は動的に決める) 10 String cursorName; 11 12 // コンストラクタ 13 public RecordEdit() { 14 recordIdList = new LinkedList<>(); 15 cursorName = "cursorTest"; 16 } 17 18 /** 宣言されたカーソルを使いフェッチしてくる **/ 19 private Linkedlist<Long> fetchRecordIds() { 20 try { 21 // トランザクションの開始 22 sqlMapClient.startTransaction(); 23 24 String query = "FORWARD " + RECS_AT_ONCE " FROM " + cursorName; 25 List<Map<String, String>> recordList = null; 26 // レコード情報の取得(FETCH) 27 recordList = sqlMapClient.queryForList("Record.fetch", query); 28 List<Long> resIdList = /** 取得したレコード情報からIDだけ抜き取る処理 **/ 29 30 // 処理が成功したならコミット 31 sqlMapClient.commitTransaction(); 32 return resIdList; 33 } finally { 34 // 成功/失敗に関わらず、トランザクションの終了 35 sqlMapClient.endTransaction(); 36 } 37 } 38 39 /** カーソルを宣言する **/ 40 private void declareCursor() { 41 try { 42 // トランザクションの開始 43 sqlMapClient.startTransaction(); 44 45 String query = cursorName + " CURSOR WITH HOLD FOR SELECT * FROM testTable"; 46 // カーソルの宣言(DECLARE) 47 sqlMapClient.queryForList("Record.declareCursor", query); 48 49 sqlMapClient.commitTransaction(); 50 } finally { 51 // ↓ここで宣言したカーソルが消える 52 sqlMapClient.endTransaction(); 53 } 54 } 55 56 /** カーソルで取得したレコードIDリストからIDを一個ずつとりだす **/ 57 private Long getNextId() { 58 // リストが空ならフェッチしてくる 59 if (recordIdList.isEmpty()) { 60 recordIdList = fetchRecordIds(cursorName); 61 } 62 // nullかidをを返す 63 return recordIdList.isEmpty() ? null : recordIdList.removeFirst(); 64 } 65 66 /** UPDATE処理を行う **/ 67 private void update(Long id) { 68 try { 69 // トランザクションの開始 70 sqlMapClient.startTransaction(); 71 72 /** 指定されたIDに対してUPDATE処理 **/ 73 74 sqlMapClient.commitTransaction(); 75 } finally { 76 sqlMapClient.endTransaction(); 77 } 78 } 79 80 /** カーソルのクローズ処理 **/ 81 private void closeCursor() { 82 // カーソルを閉じる(CLOSE) 83 sqlMapClient.queryForList("Record.closeCursor", cursorName); 84 } 85 86 /** メイン処理 **/ 87 public void execute() { 88 // カーソルを宣言 89 declareCursor(); 90 while (true) { 91 // idを取得 92 long recordId = getNextId(); 93 // 次のレコードが無いならループを抜ける 94 if (recordId == null) { 95 break; 96 } 97 // UPDATE処理を行う 98 update(recordId); 99 } 100 // 無事ループを抜けたならカーソルを閉じる 101 closeCursor(); 102 } 103}
追記
1つのトランザクションで処理
Java
1public class RecordEdit() { 2 // DBにアクセスするためのもの 3 final SqlMapClient sqlMapClient = baseDao.getSqlMapClient(); 4 // フェッチしてくる数を設定(1000件) 5 final int RECS_AT_ONCE = 1000; 6 7 // レコードIDを格納するリスト 8 Linkedlist<Long> recordIdList; 9 // カーソル名(本来は動的に決める) 10 String cursorName; 11 12 // コンストラクタ 13 public RecordEdit() { 14 recordIdList = new LinkedList<>(); 15 cursorName = "cursorTest"; 16 } 17 18 /** 宣言されたカーソルを使いフェッチしてくる **/ 19 private Linkedlist<Long> fetchRecordIds() { 20 String query = "FORWARD " + RECS_AT_ONCE " FROM " + cursorName; 21 List<Map<String, String>> recordList = null; 22 // レコード情報の取得(FETCH) 23 recordList = sqlMapClient.queryForList("Record.fetch", query); 24 List<Long> resIdList = /** 取得したレコード情報からIDだけ抜き取る処理 **/ 25 return resIdList; 26 } 27 28 /** カーソルを宣言する **/ 29 private void declareCursor() { 30 String query = cursorName + " CURSOR WITH HOLD FOR SELECT * FROM testTable"; 31 // カーソルの宣言(DECLARE) 32 sqlMapClient.queryForList("Record.declareCursor", query); 33 } 34 35 /** カーソルで取得したレコードIDリストからIDを一個ずつとりだす **/ 36 private Long getNextId() { 37 // リストが空ならフェッチしてくる 38 if (recordIdList.isEmpty()) { 39 recordIdList = fetchRecordIds(cursorName); 40 } 41 // nullかidをを返す 42 return recordIdList.isEmpty() ? null : recordIdList.removeFirst(); 43 } 44 45 /** UPDATE処理を行う **/ 46 private void update(Long id) { 47 /** 指定されたIDに対してUPDATE処理 **/ 48 } 49 50 /** カーソルのクローズ処理 **/ 51 private void closeCursor() { 52 // カーソルを閉じる(CLOSE) 53 sqlMapClient.queryForList("Record.closeCursor", cursorName); 54 } 55 56 /** メイン処理 **/ 57 public void execute() { 58 try { 59 // トランザクションの開始 60 sqlMapClient.startTransaction(); 61 // カーソルを宣言 62 declareCursor(); 63 while (true) { 64 // idを取得 65 long recordId = getNextId(); 66 // 次のレコードが無いならループを抜ける 67 if (recordId == null) { 68 break; 69 } 70 // UPDATE処理を行う 71 update(recordId); 72 // 処理が成功したなら都度コミット 73 sqlMapClient.commitTransaction(); 74 } 75 } finally { 76 // 成功/失敗に関わらず、トランザクションの終了 77 sqlMapClient.endTransaction(); 78 // 無事ループを抜けたならカーソルを閉じる 79 closeCursor(); 80 } 81 } 82}
試したこと
トランザクションを別々にせずに、同じトランザクション内で処理を行えば、カーソルでの処理はできました。
しかし、件数が多すぎるとUPDATE処理にすごく時間がかかり、あまりよろしくありません(テーブルの列が10個で1レコード40㎳ぐらい、1万レコードだと400sかかる)。
UPDATE処理は別のトランザクションで処理した方が速いというのはこれまでの試験でわかっており(1レコード15msくらい)、どうしても別々のトランザクションで処理を行いたいと考えています。
補足情報(FW/ツールのバージョンなど)
ここにより詳細な情報を記載してください。
回答2件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/02/15 11:56
2018/02/15 13:25
2018/02/16 00:02