前のネタでSQLite + SQLiteJDBCでは重いselect文を実行してしまったときにStatement.cancelメソッドが効かず、途中で止められないことが判明しました。
- 前の投稿 ... java : SQLiteではStatement.cancelが効かない
しかし今回使っているSQLiteJDBCではユーザー定義関数を作ることができます。
- 以前の投稿 ... java : SQLiteにjdbcで接続してユーザー定義関数を使う
これをうまく使えばselectの途中で例外を投げてクエリーを止めることができます。 というわけでこんなクラスを作ってみました。
package sqlitestatementclosetest; import java.sql.SQLException; import org.sqlite.Function; public class SqliteUserFunctionChanceToStop extends Function { public final long DEFAULT_CHECK_INTERVAL = 200; public final String STOPPED_MESSAGE = "SqliteUserFunctionChanceToStop : comfirmed stop methord."; private long _timeBefore; private boolean _exit; private long _checkInterval; public SqliteUserFunctionChanceToStop() { _timeBefore = 0; _exit = false; _checkInterval = DEFAULT_CHECK_INTERVAL; } public String getName() { return "ufChanceToStop"; } public void SetCheckInterval(long interval) { _checkInterval = interval; } public synchronized void stop() { _exit = true; } @Override protected void xFunc() throws SQLException { long currentTime = System.currentTimeMillis(); if(_checkInterval < (currentTime - _timeBefore) ) // synchronizedをしているのでチェックがあまりに頻繁にならないように { _timeBefore = currentTime; synchronized(this) { if(_exit) { // 次に使うときのためにリセット _timeBefore = 0; _exit = false; throw new SQLException(STOPPED_MESSAGE); //errorメソッドでは例外の種類を特定できない? //error(STOPPED_MESSAGE); } } } result(value_long(0) ); } }
これを試すのは、前のコードをちょっとかえて、
package sqlitestatementclosetest; import 前と同じなので略... public class Database implements Runnable { private static final String DB_NAME = "d:/test.db"; private Statement _st; private SqliteUserFunctionChanceToStop _ufChanceToStop; @Override public void run() { PrintStream out = System.out; Connection c = null; Random r = new Random(System.currentTimeMillis() ); try { out.println("◆データベースの用意"); 前と同じなので略... out.println("◆SabotageFunctionの登録"); 前と同じなので略... out.println("◆SqliteUserFunctionChanceToStopの登録"); _ufChanceToStop = new SqliteUserFunctionChanceToStop(); Function.create(c, _ufChanceToStop.getName(), _ufChanceToStop); // これはロックを得るときのタイムアウトでselect文を途中で止める効果はない _st.setQueryTimeout(3 * 1000); out.println("◆ゆっくりとselect"); out.println(" !エンターキーを押すと停止"); ResultSet res = _st.executeQuery( "select ufSabotage(col1) as col1s, col2 from tmain order by ufChanceToStop(col1)" ); 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); } } } out.println("◆データベーススレッド終了"); } public void stop() { assert _ufChanceToStop != null; _ufChanceToStop.stop(); } private void printException(String text, Exception exc) { 前と同じなので略... } private void printSqlException(String text, SQLException exc) { 前と同じなので略... } }
select文とstopメソッドの中身が少々変わっています。 実行結果はこんな感じ。
◆データベースの用意
◆SabotageFunctionの登録
◆SqliteUserFunctionChanceToStopの登録
◆ゆっくりとselect
!エンターキーを押すと停止
← ここでエンターキーを押した
◆Stopメソッドを呼びました。
SQL例外
◆データベーススレッド終了
[SQLITE_ERROR] SQL error or missing database (java.sql.SQLException: SqliteUserFunctionChanceToStop : comfirmed stop methord.)
◆プログラム終了。
code = 0
state = null
org.sqlite.DB.newSQLException(DB.java:383)
org.sqlite.DB.newSQLException(DB.java:387)
org.sqlite.DB.execute(DB.java:342)
org.sqlite.Stmt.exec(Stmt.java:65)
org.sqlite.Stmt.executeQuery(Stmt.java:122)
sqlitestatementclosetest.Database.run(Database.java:72)
java.lang.Thread.run(Thread.java:722)
ちゃんと止まりました。
このコードを使う場合の注意点は3つ。 1つ目は「xFunc中に投げた例外メッセージの判別の仕方」です。
try { ResultSet res = ステートメント.executeQuery(クエリー); } catch(SQLException exc) { なんたら }
とした場合、excの中に直接xFunc中に投げた例外は入りません。 先ほどの実行結果のとおり、exc.getMessageの中身は
[SQLITE_ERROR] SQL error or missing database
(java.sql.SQLException: xFuncで投げた例外のメッセージ)
となっています。 SQLiteJDBCはjniを使っているので、その過程でこうなったのだと思われます。 中断したときの例外か別の例外かを見分けるときはexc.getMessageの文字列を調べる必要があります。
2つ目は、当然ながら「SqliteUserFunctionChanceToStop.xFuncが呼ばれない限りクエリーが止まる事はない」ということ。 クエリーへの仕込み方を間違えるとxFuncに処理が来ません。 order byやselect句に仕込んだ場合は行が見つかったときしかxFuncが呼ばれないので注意。 whereに上手く仕込めば毎回呼ばれる可能性はあるかもしれませんが、この辺はSQLite本体の最適化次第で変わってしまうかもしれません。 試してませんが、SQLiteはデータベースとしてはシンプルな作りなので、今のバージョンでは「クエリーのここに仕込めば毎回呼ばれる」というような工夫の仕方はあるかもしれないですね。 しかしそういうのはSQLiteのバージョンが上がると無意味になるかもしれません。 ChanceToStopという名前の通り「止める機会があるかも?」というくらいの期待感で使ってください。
3つ目は「クエリーによってxFuncが呼ばれるタイミングが違う」ということです。 これは前の投稿の通りです。 sql文の書き方によってStatement.executeQueryの時点で呼ばれる場合とResultSet.nextの時点で呼ばれる場合があります。 どちらで例外が発生してもいいようにコーディングしましょう。
...これでクエリーを途中で止めれるようにはなりました。 しかしなんというか、SQLiteというかSQLiteJDBCにべったりのコードですね。 ここまでしなくちゃならないくらいならSQLite以外の高機能なデータベースを選んだ方がいいのかも?