竹形誠司 ブログ
Java+MySQL+Tomcat    »トピック一覧
掲示板へのスパムが多いため、「ご質問」のコーナーはユーザー登録制とさせていただきました。お手数ですが、上の「新規ユーザーの登録」メニューより登録をお願いします。
帳票Web
アプリケーション

受注開発始めました
詳しくは こちら
竹形 誠司 著/ラトルズ刊
JSP帳票アプリケーション実践開発入門
JSP帳票アプリケーション
実践開発入門

JSP業務アプリケーション短期開発入門
JSP業務アプリケーション
短期開発入門

Java+MySQL+Tomcatで始めるWebアプリケーション構築入門
Java+MySQL+Tomcatで始めるWebアプリケーション構築入門

Java+MySQL+Tomcatで作る掲示板とブログ
Java+MySQL+Tomcatで作る
掲示板とブログ
SQLの実行にはStatementよりPreparedStatementを使おう
by 竹形 誠司[takegata]
MySQLのバージョン4.1からPreparedStatementが使えるようになりました。私が書いた本のうち、「Java+MySQL+Tomcatで作る掲示板とブログ」は、まだPreparedStatementが使えないバージョンを元に書いているので、ここで説明しようと思います。

PreparedStatementの使い方
次のようSQL文で作成したなデータベースがあったとします。
create database pstmt_test;
use pstmt_test;
grant all on pstmt_test.* to Mulder@localhost identified by 'TrustNo1';
create table person(id int auto_increment primary key,name varchar(10));
insert into person set name='一朗';
insert into person set name='二郎';
insert into person set name='三郎';
personテーブルの内容はこんな感じです。
mysql> select * from person;
+----+------+
| id | name |
+----+------+
|  1 | 一朗 |
|  2 | 二郎 |
|  3 | 三郎 |
+----+------+
3 rows in set (0.00 sec)
まず、PreparedStatementを使わない方法でアクセスするコードを書いてみましょう。
import java.sql.*;

public class PstmtTest1{

  public static void main(String args[]){
      if(args.length!=1){
        System.out.print("検索文字列を指定してください。");
        System.exit(1);
      }
      String aName = args[0];
    try{
      Class.forName("com.mysql.jdbc.Driver");
      String strConn="jdbc:mysql://localhost/pstmt_test"
          + "?user=Mulder&password=TrustNo1"
          + "&useUnicode=true&characterEncoding=MS932";
      Connection conn =DriverManager.getConnection(strConn);
      String sql="SELECT * FROM person where name='" +aName +"'";//★SQL文の作成
      Statement stmt = conn.createStatement();  //★Statementクラス
      ResultSet rs = stmt.executeQuery(sql);  //★SQL文の実行
      while(rs.next()){
        System.out.print(rs.getString("id")+":");
        System.out.println(rs.getString("name"));
      }
    }catch(Exception e){
      e.printStackTrace();
    }
  }
}
PreparedStatementを使う場合は、まずSQL文の中にクエスチョンマーク(?)の形でパラメータを埋め込んで仮のSQL文を作ります。
String sql="SELECT * FROM person where name=?";
次に、この仮のSQL文をConnectionクラスのprepareStatementメソッドに与えてPreparedStatementオブジェクトを取得します。
PreparedStatement pstmt = conn.createStatement(sql);
そして、仮に"?"を入れておいたパラメータに実際の値を割り当てます。
pstmt.setString(1,aName);
パラメータが文字列の場合はsetStringメソッドを使いますが、整数の場合はsetIntメソッド、浮動小数点の場合はsetFloatなど、変数の型に応じてメソッドを使い分けます。最初の引数(上の例では1)は、仮のSQL文の中でパラメータ(?)が表れる位置を表しています。仮のSQL文にパラメータがが3回出現する場合は、最初のパラメータが1、次のパラメータが2、最後のパラメータが3になります。

PreparedStatementを使ったコードは次のようになります。
import java.sql.*;

public class PstmtTest2{

  public static void main(String args[]){
      if(args.length!=1){
        System.out.print("検索文字列を指定してください。");
        System.exit(1);
      }
      String aName = args[0];
    try{
      Class.forName("com.mysql.jdbc.Driver");
      String strConn="jdbc:mysql://localhost/pstmt_test"
          + "?user=Mulder&password=TrustNo1"
          + "&useUnicode=true&characterEncoding=MS932";
      Connection conn =DriverManager.getConnection(strConn);
      String sql="SELECT * FROM person where name=?";  //★仮のSQL文
      PreparedStatement pstmt = conn.prepareStatement(sql);
      pstmt.setString(1,aName); //★パラメータに実際の値を割り当て
      ResultSet rs = pstmt.executeQuery();
      while(rs.next()){
        System.out.print(rs.getString("id")+":");
        System.out.println(rs.getString("name"));
      }
    }catch(Exception e){
      e.printStackTrace();
    }
  }
}

SQLインジェクションの防止
PreparedStatementを使用するメリットの1つはSQLインジェクション攻撃に対して強いことです。たとえば、Statementオブジェクトで実行するSQL文を次のようなコードで作成したとします。
String sql = "SELECT * FROM person WHERE name ='" + aName + "'";
aNameに"一朗"が入っていた場合、実行されるSQL文は次のようなものになります。
SELECT * FROM person WHERE name = '一朗'
ところが、aNameが次のような文字列だったらどうでしょう。
' OR name LIKE '%
実行されるSQL文は次のようになります。
SELECT * FROM person WHERE name = '' OR name LIKE '%'
検索文字列にシングルクオートを含めることで、SQL文の検索文字列をそこで終らせ、別の検索条件(ここでは OR name LIKE '%')を潜り込ませることに成功しています。このような方法でデータベースから不正に情報を引き出す手口を「SQLインジェクション」といいます。

PreparedStatementでは、パラメータに実際の値を割り当てる際に、JDBCドライバがシングルクオートなどを自動的にエスケープシーケンスに変換してくれるので、SQL文の意味が変わってしまうことがありません。

SQL文の見易さ
クエスチョンマークを使って仮のSQL文が書けるので、SQL文の見通しが非常に良くなります。
たとえば、拙著「Java+MySQL+Tomcatで作る掲示板とブログ」のP.75に次のようなコードがあります。
String strSql="INSERT INTO bbs (date_time,author,subject,body,password) "
      +"VALUES ("+dateNow.getTime() +",'" //★非常に分かりにくい
      + TextConv.beforeSql(strAuthor) + "','"
      + TextConv.beforeSql(strSubject) + "','"
      + TextConv.beforeSql(strBody) + "',"
      + "password('" + TextConv.beforeSql(strPassword) + "') )";
stmt.executeUpdate(strSql);
PreparedStatementを使えば、次のように書くことができます。
String strSql="INSERT INTO bbs (date_time,author,subject,body,password) "
      +"VALUES (?,?,?,?,password(?))";  //★すっきり
PreparedStatement pstmt = conn.prepareStatement(strSql);
pstmt.setLong(1,dateNow.getTime());
pstmt.setString(2,strAuthor);
pstmt.setString(3,strSubject);
pstmt.setString(4,strBody);
pstmt.setString(5,strPassword);
pstmt.executeUpdate();
特殊文字のエスケープは自動的に行われるので、TextConv.beforeSqlメソッドを使用する必要はありません。シングルクオートやダブルクオート、カンマ、+記号が入り乱れて分かりにくい部分がすっきりしたのが分かると思います。
投稿:竹形 誠司[takegata]/2009年 01月 27日 02時 48分 /更新:2009年 01月 27日 16時 07分
PreparedStatement でも LIKEを使う場合は "%"と"_"のエスケープが必要
by 竹形 誠司[takegata]
PreparedStatementはエスケープが不要と書きましたが、LIKE構文を使う場合はワイルドカードとして使われる"%"と"_"をエスケープする必要がありました。LIKE節の中では "%" と "_" は任意の文字に該当するワイルドカードとして扱われるため、文字としての"%"や"_"を検索したい場合は\を付けて "\%" または "\_" にする必要があるのです。たとえば、こんな感じです。
String strSql = "SELECT * FROM test WHERE str LIKE ?";
PreparedStatement pstmt= conn.prepareStatement(strSql);
strSearch = strSearch .replaceAll("%","\\\\%").replaceAll("_","\\\\_");
pstmt.setString(1,"%" + strSearch + "%");
ResultSet rs = pstmt.executeQuery();
注意点は、Javaの文字列として "\" を表記するには "\\" のエスケープシーケンスを使う必要があり、更に、replaceAllの置換文字列では "\" が特殊文字として扱われるため、ここでも "\\" のエスケープシーケンスに置き換える必要があるということです。そのため、"\\\\" のように\を4つ重ねて書く必要があります。
投稿:竹形 誠司[takegata]/2009年 04月 15日 14時 17分 /更新:2009年 04月 15日 14時 17分