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は発行されないようなので「サポートされているけど失敗する」というしょっぱい状態でした。