この連載では、Javaのデータベース・アクセスAPIである「JDBC」の機能を、サンプルコードを交えて解説していきます。また、J2EEにおけるJDBCの位置付けや、JDBCを利用するさまざまなテクノロジについても解説していく予定です。前提知識としては、Javaとリレーショナル・データベースに関するベーシックな知識があれば十分です。
第2回「JDBCによるDBへの接続と検索の実行」では、select文の実行を、そして第3回「JDBCによる更新処理の実行」では、DDL文やDML文の実行を取り上げました。第4回の今回は、SQL文をプリコンパイルするプリペアド・ステートメントの利用方法、そして、ストアド・プロシージャの実行方法を紹介しましょう。
ここでは、SQL文をプリコンパイルするために使用するプリペアド・ステートメントについて見ていきましょう。
サンプル・コードは、where句の中のパラメータだけが異なるselect文を、forループで1万回実行し、その実行時間を出力するものです。mainメソッドの引数がsの場合は、通常のステートメント(java.sql.Statement インターフェイス)を使用し、引数がpsの場合は、プリペアド・ステートメント(java.sql.PreparedStatement インターフェイス)を使用します。
(※赤字部分はコメントです。コードの一部ではありませんのでご注意ください)
// Javaデータアクセスの基礎 サンプル・コード(4)
// プリペアド・ステートメントとステートメントの比較
// JDBC APIをインポート
import java.sql.*;
class JavaDataAccess04 {
public static void main (String args[]) {
Connection conn = null;
Statement stmt = null;
PreparedStatement pstmt = null;
ResultSet rset = null;
String sql_str = null;
long start= 0;
long end = 0;
if ( (args.length < 1) ||
!( args[0].equals("s")
|| args[0].equals("ps") ) ) {
System.out.println("パラメータに \"s\"
または \"ps\" を" + "指定してください...");
} else {
start = System.currentTimeMillis();
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
System.out.println("JDBCドライバをロードしました...");
// Oracle8i Databaseに接続
conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORCL",
"scott", "tiger");
System.out.println("データベースに接続しました...");
(↑ここまでで、データベースに接続するための初期処理を行います)
(↓mainメソッドの引数で処理を振り分けます。通常のステートメントを1万回実行します)
if ( args[0].equals("s") ) {
stmt = conn.createStatement();
System.out.println("ステートメントを作成しました...");
for ( int i=0 ; i<10000 ; i++ ) {
// select文を実行
sql_str = "select EMPNO,
ENAME, SAL from EMP " + "where EMPNO = " + i;
rset = stmt.executeQuery(sql_str);
// 問合せ結果を表示
while ( rset.next() ) {
System.out.println(rset.getInt("EMPNO")
+ "\t"
+ rset.getString("ENAME")
+ "\t"
+ rset.getInt("SAL"));
} // while
} // for
// 結果セット、ステートメントをクローズ
rset.close();
stmt.close();
(↓ここからは、上と同様の実行内容ですが、プリコンパイルしたものを1万回繰り返しています)
} else if ( args[0].equals("ps")
) {
// select文をプリコンパイル
sql_str = "select EMPNO,
ENAME, SAL from EMP "
+ "where EMPNO
= ?";
pstmt = conn.prepareStatement(sql_str);
System.out.println("プリペアド・ステートメントを作成しました...");
for (int i = 0; i < 10000; i++) {
// パラメータを設定
pstmt.setInt(1, i);
// select文を実行
rset = pstmt.executeQuery();
// 問合せ結果を表示
while ( rset.next() ) {
System.out.println(rset.getInt("EMPNO")
+ "\t"
+ rset.getString("ENAME")
+ "\t"
+ rset.getInt("SAL"));
} // while
} // for
(↓最後に終了と例外の処理を行います)
// 結果セット、プリペアド・ステートメントをクローズ
rset.close();
pstmt.close();
} // else if
conn.close();
System.out.println("接続をクローズしました...");
end = System.currentTimeMillis();
System.out.println("実行時間: " + (end - start)
+ "ミリ秒");
// 例外を処理
} catch (Exception ex) {
System.out.println("例外が発生しました...");
// エラー・メッセージを出力
System.out.print(ex.toString());
try {
if (rset != null) { rset.close(); }
if (pstmt != null) { pstmt.close();
}
if (stmt != null) { stmt.close(); }
if (conn != null) { conn.close(); }
} catch (SQLException se) {}
} // catch
} // else if
} // main
} |
リスト JavaDataAccess04.java
|
上記のコードの実行結果は、次のようになります。同じような処理を2回実行していますが、2回目のほうが高速なことが分かります。
1回目 通常のステートメント |
2回目 プリペアド・ステートメント |
C:\JDBC>java
JavaDataAccess04 s
JDBCドライバをロードしました...
データベースに接続しました...
ステートメントを作成しました...
7369 SMITH 800
7499 ALLEN 1600
7521 WARD 1250
7566 JONES 2975
7654 MARTIN 1250
7698 BLAKE 2850
7782 CLARK 2450
7788 SCOTT 3000
7839 KING 5000
7844 TURNER 1500
7876 ADAMS 1100
7900 JAMES 950
7902 FORD 3000
7934 MILLER 1300
接続をクローズしました...
実行時間: 72500ミリ秒 |
C:\JDBC>java
JavaDataAccess04 ps
JDBCドライバをロードしました...
データベースに接続しました...
プリペアド・ステートメントを作成しました...
7369 SMITH 800
7499 ALLEN 1600
7521 WARD 1250
7566 JONES 2975
7654 MARTIN 1250
7698 BLAKE 2850
7782 CLARK 2450
7788 SCOTT 3000
7839 KING 5000
7844 TURNER 1500
7876 ADAMS 1100
7900 JAMES 950
7902 FORD 3000
7934 MILLER 1300
接続をクローズしました...
実行時間: 39160ミリ秒 |
なぜこのように、実行速度に差がでたのでしょうか。その内容を理解するため、順を追ってコードを見てみましょう。
mainメソッドの引数がsの場合には、第2回で紹介したように、ステートメントを使用してselect文を実行しています。
stmt =
conn.createStatement();
System.out.println("ステートメントを作成しました...");
for (int i=0; i<10000; i++) {
// select文を実行
sql_str = "select EMPNO, ENAME, SAL from EMP "
+ "where EMPNO = " + i;
rset = stmt.executeQuery(sql_str); |
ステートメントを使用する場合、createStatement()メソッドでStatementオブジェクトを作成するときには、SQL文を渡しません。executeQuery()メソッド(またはexecuteUpdate()、execute()メソッド)でSQL文を実行するときに、そのメソッドのパラメータとしてSQL文を渡します。
データベースは、executeXXX()メソッドから渡されたSQL文を解析し、そのSQL文を最も効率的に実行するための実行計画(表や索引(index)へのアクセス方法や、結合(join)の順序など) を決定します。このステップを、SQL文の「最適化(optimize)」、あるいは「コンパイル」といいます。そして、SQL文の最適化の後に、決定された実行計画に基づいて、SQL文を実行するわけです。
このサンプル・コードでは、SQL文の最適化と実行が1万回繰り返されていることになります。ですが、ここで実行しているSQL文はwhere句のパラメータが異なるだけであり、その実行計画は同一であることは明らかですね。にもかかわらず、SQL文の最適化を毎回行うのは効率が悪いと思うでしょう。この問題を解決するのが、プリペアド・ステートメントなのです。
では、プリペアド・ステートメントは、どのように利用するのでしょう? mainメソッドの引数がpsの場合には、プリペアド・ステートメントを使用して同一の処理を実行しています。
プリペアド・ステートメントを使用する場合、createStatement()メソッドではなくprepareStatement()メソッドを使用します。ここで注目してほしいのは、prepareStatement()メソッドのパラメータでSQL文を渡している点です。これにより、prepareStatement()メソッドが呼び出された時点で、あらかじめSQL文のコンパイルを行うのです。SQL文自体の実行前であるため、このことを指して「プリコンパイルする」、あるいは「準備する(prepare)」といいます。
// select文をプリコンパイル
sql_str = "select EMPNO, ENAME, SAL from EMP "
+ "where EMPNO = ?";
pstmt = conn.prepareStatement(sql_str);
System.out.println("プリペアド・ステートメントを作成しました..."); |
prepareStatement()メソッドに渡すSQL文では、パラメータのプレース・ホルダとして“?”を用います。当然ながら、実際にSQL文を実行する前に、すべてのパラメータをセットする必要があります。パラメータのセットには、PreparedStatementオブジェクトのsetXXX()メソッドを用います。setXXX()の第1パラメータは(1から始まる)パラメータの位置番号、第2パラメータはパラメータの値となります。第1回で紹介したように、Javaデータ型とJDBCデータ型の間のマッピングを考慮して、適切なsetXXX()を用いましょう。
for (int i =
0; i < 10000; i++) {
// パラメータを設定
pstmt.setInt(1, i); |
すべてのパラメータをセットしたら、executeXXX()メソッドで、実際にSQL文を実行します。サンプル・コードでは、select文を実行するので、executeQuery()メソッドを用いています。このとき、SQL文を渡す必要はありません。すでにプリコンパイルされたSQL文を単に実行するだけであるため、ステートメントの場合より効率的にSQL文を実行することが可能です。
// select文を実行
rset = pstmt.executeQuery(); |
筆者の環境では、プリペアド・ステートメントを利用することによって、ステートメントの場合に比べて実行時間が約46%短縮されました。環境によって数値に違いはあるでしょうが、この傾向は変わらないはずです。プリペアド・ステートメントを利用できる場合は、極力利用するようにしましょう。