jdkのドキュメントを見ると、Statement.cancelメソッドで実行中のSQL文を停止できるようです。 ただし、apiリファレンスには「DBMSおよびドライバの両方がSQL文の終了をサポートする場合に、このStatementオブジェクトを取り消します。」との記述があります。 サポートしていない場合はSQLFeatureNotSupportedExceptionが投げられる模様。
SQLite + SQLiteJDBCでcancelができるか試してみました。 SQLiteJDBCの入手先は、
試したコードはこんな感じです。 まずはデータベースを扱うクラス。 Statement.cancelメソッドは別スレッドから停止させるためのものなので、Runnableを実装しています。 メインスレッド以外にデータベース処理用のスレッドを作り、selectが動いたらメインスレッドから停止させます。
// Database.java package sqlitestatementclosetest; import java.io.PrintStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.util.Random; import org.sqlite.Function; public class Database implements Runnable { private static final String DB_NAME = "d:/test.db"; private Statement _st; @Override public void run() { PrintStream out = System.out; Connection c = null; Random r = new Random(System.currentTimeMillis() ); try { out.println("◆データベースの用意"); Class.forName("org.sqlite.JDBC"); c = DriverManager.getConnection("jdbc:sqlite:" + DB_NAME); c.setAutoCommit(false); String sqlDropTable = "drop table if exists tmain"; String sqlCreateTable = "create table tmain (col1 integer, col2 text)"; String[] col2s = {"住宅", "アパート", "豪邸", "道路", "高速道路", "信号", "線路", "踏み切り", "バス停", "商店", "スーパー", "デパート", "アンテナショップ", "地下街", "駅", "田んぼ", "工場", "町工場", "発電所", "空港", "漁港", "灯台"}; _st = c.createStatement(); _st.executeUpdate(sqlDropTable); _st.executeUpdate(sqlCreateTable); PreparedStatement ps = c.prepareStatement( "insert into tmain values (?, ?)" ); for(int i = 0; i < 10000; i++) { ps.setInt(1, r.nextInt() ); ps.setString(2, "" + Math.abs(r.nextInt() ) + "番目の" + col2s[i % col2s.length] ); ps.executeUpdate(); } c.commit(); out.println("◆SabotageFunctionの登録"); SabotageFunction ufSabotage = new SabotageFunction(); Function.create(c, ufSabotage.getName(), ufSabotage); out.println("◆ゆっくりとselect"); out.println(" !エンターキーを押すと停止"); ResultSet res = _st.executeQuery( "select ufSabotage(col1) as col1s, col2 from tmain" ); int line = 1; while(res.next() ) { out.println(String.format( "> %03d : %12d, %20s", line, res.getInt("col1s"), res.getString("col2") )); line++; } } catch(ClassNotFoundException exc) { printException("jdbcドライバの読み込みエラー", exc); } catch(SQLFeatureNotSupportedException exc) { printSqlException("SQL例外(JDBCドライバがサポートしない機能を実行)", exc); } catch(SQLException exc) { printSqlException("SQL例外", exc); } finally { if(c != null) { try { c.close(); } catch (SQLException exc) { printSqlException("SQLコネクションクローズ失敗", exc); } } } } public void stop() { assert _st != null; try { _st.cancel(); } catch (SQLException exc) { printSqlException("Statementのcancel失敗", exc); } } private void printException(String text, Exception exc) { StringBuilder sb = new StringBuilder(text); sb.append("\n\t"); sb.append(exc.getMessage() ); sb.append('\n'); for(StackTraceElement elem : exc.getStackTrace() ) sb.append('\t').append(elem.toString() ).append('\n'); System.err.println(sb); } private void printSqlException(String text, SQLException exc) { StringBuilder sb = new StringBuilder(text); sb.append("\n\t"); sb.append(exc.getMessage() ); sb.append("\n\t"); sb.append("code = "); sb.append(exc.getErrorCode() ); sb.append("\n\t"); sb.append("state = "); sb.append(exc.getSQLState() ); sb.append('\n'); for(StackTraceElement elem : exc.getStackTrace() ) sb.append('\t').append(elem.toString() ).append('\n'); System.err.println(sb); } }
メインスレッドはデータベースのスレッドを実行してからエンターキー待ち。 エンターキーが押されたらcancelを呼び出します。
// SqliteStatementCloseTest.java package sqlitestatementclosetest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class SqliteStatementCloseTest { public static void main(String[] args) { Database db = new Database(); Thread th = new Thread(db); th.start(); WaitEnterKey(); db.stop(); System.out.println("Stopメソッドを呼びました。"); try { th.join(); } catch (InterruptedException exc) { exc.printStackTrace(); } System.out.println("プログラム終了。"); } public static void WaitEnterKey() { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try { br.readLine(); } catch(IOException exc) { exc.printStackTrace(); } } }
ufSabotageはselectの処理時間をわざと遅くするユーザー定義関数です。 ユーザー定義関数の作り方については前の投稿を見てください。
// SabotageFunction.java package sqlitestatementclosetest; import java.sql.SQLException; import org.sqlite.Function; public class SabotageFunction extends Function { public String getName() { return "ufSabotage"; } @Override protected void xFunc() throws SQLException { try { Thread.sleep(300); } catch (InterruptedException exc) { exc.printStackTrace(); } result(value_int(0) ); } }
結果は次の通り、
◆データベースの用意
◆SabotageFunctionの登録
◆ゆっくりとselect
!エンターキーを押すと停止
> 001 : -1797261181, 613391689番目の住宅
> 002 : 1969736450, 1226649321番目のアパート
> 003 : -1673489017, 1838990850番目の豪邸
> 004 : -740095700, 1463971762番目の道路
← ここでエンターキーを押した
Stopメソッドを呼びました。
> 005 : 2024542586, 1395106411番目の高速道路
SQL例外
プログラム終了。
[SQLITE_INTERRUPT] Operation terminated by sqlite3_interrupt() (interrupted)
code = 0
state = null
org.sqlite.DB.newSQLException(DB.java:383)
org.sqlite.DB.newSQLException(DB.java:387)
org.sqlite.DB.throwex(DB.java:374)
org.sqlite.RS.next(RS.java:152)
sqlitestatementclosetest.Database.run(Database.java:68)
java.lang.Thread.run(Thread.java:722)
思っていたのと処理の様子がぜんぜん違いました。 select文が全部終わってからResultSetの処理に行くと思ったら、select文は即終了、ResultSetを取り出すところでウェイトがかかっています。 ResultSet.nextが呼ばれるタイミングでSabotageFunction.xFuncが呼ばれているんですね。 で、4行見たところでエンターを押したらSQLException(SQLITE_INTERRUPT)が発生しました。 (標準出力とエラー出力が混ざってしまったけど、それは本筋で無いので流しで。)
Statement.cancelメソッドはSQLiteJDBCでサポートされているようです。 で、cancelされると例外が投げられます。 そのときエラーコードやSQLStateには値が設定されないようです。 cancelと他の例外を場合分けするにはエラーメッセージの文字列比較をするしか無いようですね。
catch(SQLException exc) { if(exc.getMessage().startsWith("[SQLITE_INTERRUPT]") ) { System.out.print("cancelされました。"); } }
しかしすごく重いSQL文を実行したときに、はたしてcancelできるものかどうか? Statement.executeQueryを止めれるかどうか確かめたかったんだよなぁ...
で、思いつきました。 select文にorder byを付けたら、executeQueryが終わるまで先に進まないんじゃないですかね?
select ufSabotage(col1) as col1s, col2 from tmain order by col1
やってみたら思ったとおりでした。 ResultSetの表示の前でウェイトがかかります。 肝心のcancelは...?
◆データベースの用意
◆SabotageFunctionの登録
◆ゆっくりとselect
!エンターキーを押すと停止
← ここでエンターキーを押した
Statementのcancel失敗
Stopメソッドを呼びました。
ResultSet closed
code = 0
state = null
org.sqlite.RS.checkOpen(RS.java:63)
org.sqlite.Stmt.cancel(Stmt.java:254)
sqlitestatementclosetest.Database.stop(Database.java:113)
sqlitestatementclosetest.SqliteStatementCloseTest.main(SqliteStatementCloseTest.java:28)
失敗しました。 そっちは思ったとおりじゃない。 デバッガでSabotageFunction.xFuncにブレイクポイントを設定してみたら、データベース用のスレッドは元気に働いていました。 どうやらSQLiteJDBCでは重いsql文を途中で止めることはできないようです。 SQLFeatureNotSupportedExceptionは発行されないようなので「サポートされているけど失敗する」というしょっぱい状態でした。