【SQL】PreparedStatementで、検索条件が複数あるが、項目がなかった場合、その項目を無視するには...

初めに

検索条件が複数あり、いずれの項目も必須でない場合、
項目が入力されているかどうかをチェックし、それに合わせてSQL 文の WHERE 句の内容を
ツギハギすることがよくあると思う。

【例】
String sql = "SELECT * FROM Person";
if (id != null) {
   sql = sql + "WHERE id=" + id;
}

でも、これはめんどいし、きれいじゃないし、PreparedStatementの良さが発揮しづらいし。。。
で、こんな時に、PreparedStatementを使う方法をメモしておく。

案1:WHERE 句の内容全てを 括弧で囲み、最後に「IS NOT FALSE」を付ける

WHERE (id = ? AND name = ?) IS NOT FALSE

ってすると、null をセットした項目は、検索条件として使われないかのようになる

 →欠点:DBによっては使えないらしい(以下の参考文献を参照のこと)

参考文献

 * 以下のサイトでは、DBに依存するらしい
  + PostgreSQL : 正常に動く
  + Oracle     : 動かない
http://www.artonx.org/collabo/backyard/?PreparedStatementAndNull

案1':? IS NULLを追加する

WHERE (id = ? OR ? IS NULL) AND (name = ? OR ? IS NULL)

 →欠点:同じ検索条件を2回入れないといけない。

案2:NULLIFとCOALESCEを使用する

WHERE NULLIF(id, '') = COALESCE(?, NULLIF(id, '')) AND NULLIF(name, '') = COALESCE(?, NULLIF(name, ''))

ってすると、null をセットした項目は、検索条件として使われないかのようになる
(「?」は、それぞれの検索条件)

 →欠点:「空文字」を検索できない(詳細は以下の【考察】を参照のこと)

補足

 * NULLIF(値1, 値2) : 
 + 値1が値2と等しい場合は、NULL を返す
 + 値1が値2と等しくない場合は、値1 を返す
 * COALESCE(値1, 値2) : 最初の null でないデータを返す
http://homepage2.nifty.com/sak/w_sak3/doc/sysbrd/psql_k15.htm

考察

NULLIF(id, '') = COALESCE(?, NULLIF(id, ''))
~~~~~~~~~~~~~~               ~~~~~~~~~~~~~~
について考える。

まず、波線部分の「NULLIF(id, '')」は、以下のようになる。
 + idの項目が空文字だったら、NULLを返す。
 + idの項目がNULLだったら、結局、NULLを返す。
 + idの項目が値(例えば「id='X000001'」)があったら、その値('X000001')を返す。

で、「COALESCE(?, NULLIF(id, ''))」は、以下のようになる。
 + 検索条件(?部分)が空文字の場合、COALESCE(【空文字】, 【NULL】)で「空文字」になる
  →最終的に「NULL = 空文字」となり、成立しない(★空文字は検索できない★)
 + 検索条件(?部分)がNULLの場合、、COALESCE(【NULL】, 【NULL】)で「NULL」になる
  →最終的に「NULL = NULL」となり、成立(OK)
 + 検索条件(?部分)が何らかの値の場合、、COALESCE(値1, 値2)で「値1」になる
  →最終的に「値1 = 値2」となり、検索条件があえば検索され、条件が合わなければ検索に引っかからない(OK)



使用するテーブル

CREATE TABLE person
(
  id character(8) NOT NULL,
  name character varying(100),
  sex character(1),
  updatedate timestamp without time zone,
  CONSTRAINT person_pkey PRIMARY KEY (id)
)

サンプル

 * ドライバなどの設定については、以下の関連記事を参照のこと。
http://blogs.yahoo.co.jp/dk521123/33317642.html

解決法1のサンプル

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class SamplePreparedStatement {
   public static void main(String[] args) {
      try {
         System.out.println("Start");
         SamplePreparedStatement.dbAccess("X0000010", null, null);
         SamplePreparedStatement.dbAccess(null, null, "f");
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         System.out.println("Finished");
      }
   }

   private static void dbAccess(String id, String name, String sex) throws Exception {
      try {
         Class.forName("org.postgresql.Driver");

         try (Connection connection = DriverManager.getConnection(
               "jdbc:postgresql://localhost:5432/Sample", "user", "password");
               PreparedStatement statement = connection
                     .prepareStatement(
                           "SELECT * FROM Person"
                           + " WHERE (id = ? AND name = ? AND sex = ?) IS NOT FALSE");) { // ★差異はここだけ★
            statement.setString(1, id);
            statement.setString(2, name);
            statement.setString(3, sex);
            try (ResultSet resultSet = statement.executeQuery();) {
               while (resultSet.next()) {
                  System.out.println("=============");
                  System.out.println(resultSet.getString("id"));
                  System.out.println(resultSet.getString("name"));
                  System.out.println(resultSet.getString("sex"));
                  System.out.println(resultSet.getString("updateDate"));
                  System.out.println("=============");
               }
            } catch (Exception ex) {
               throw ex;
            }
         } catch (Exception ex) {
            throw ex;
         }
      } catch (Exception ex) {
         throw ex;
      }
   }
}

解決法2のサンプル

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class SamplePreparedStatement {
   public static void main(String[] args) {
      try {
         System.out.println("Start");
         SamplePreparedStatement.dbAccess("X0000010", null, null);
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         System.out.println("Finished");
      }
   }

   private static void dbAccess(String id, String name, String sex) throws Exception {
      try {
         Class.forName("org.postgresql.Driver");

         try (Connection connection = DriverManager.getConnection(
               "jdbc:postgresql://localhost:5432/Sample", "user", "password");
               PreparedStatement statement = connection
                     .prepareStatement(
                           "SELECT * FROM Person"
                           + " WHERE NULLIF(id, '') = COALESCE(?, NULLIF(id, ''))" // ★差異はここ★
                                 + " AND NULLIF(name, '') = COALESCE(?, NULLIF(name, ''))" // ★差異はここ★
                                 + " AND NULLIF(sex, '') = COALESCE(?, NULLIF(sex, ''))");) { // ★差異はここ★
            statement.setString(1, id);
            statement.setString(2, name);
            statement.setString(3, sex);
            try (ResultSet resultSet = statement.executeQuery();) {
               while (resultSet.next()) {
                  System.out.println("=============");
                  System.out.println(resultSet.getString("id"));
                  System.out.println(resultSet.getString("name"));
                  System.out.println(resultSet.getString("sex"));
                  System.out.println(resultSet.getString("updateDate"));
                  System.out.println("=============");
               }
            } catch (Exception ex) {
               throw ex;
            }
         } catch (Exception ex) {
            throw ex;
         }
      } catch (Exception ex) {
         throw ex;
      }
   }
}

補足

 * SQL serverであれば、以下のような方法もあるらしい。

WHERE id = ISNULL(@id, id)
http://qiita.com/sk_3122/items/63c84a94cad566c02637