PreparedStatement IN句の代替?


342

INインスタンスでSQL 句を使用するための最良の回避策は何ですか?これは、java.sql.PreparedStatementSQLインジェクション攻撃のセキュリティ問題のため、複数の値に対してサポートされていません。1つの?プレースホルダーは、値のリストではなく1つの値を表します。

次のSQLステートメントについて考えてみます。

SELECT my_column FROM my_table where search_column IN (?)

使用することpreparedStatement.setString( 1, "'A', 'B', 'C'" );?、そもそも使用する理由の回避策での、実際には機能しない試みです。

どのような回避策がありますか?


1
オスカー、(?、?、....)の動的生成は、IN句が必要な場合の最も簡単な回避策だと思いますが、特定のケースではパフォーマンスが十分だったので、個別の呼び出しに任せました。
Chris Mazzola、

6
準備済みステートメントの利点の1つは、効率を上げるために一度コンパイルできることです。in句を動的にすることで、準備されたステートメントを効果的に無効にします。

2
実際、これはMySQLで機能します(setObjectを使用してStringの配列をパラメーター値として設定します)。どのDBを使用していますか?
フランス、2012


関連する質問は次のとおりです。stackoverflow.com
q

回答:


194

利用可能なさまざまなオプションの分析、およびそれぞれの長所と短所はこちらから入手できます

推奨されるオプションは次のとおりです。

  • 準備しSELECT my_column FROM my_table WHERE search_column = ?、各値に対して実行し、結果をクライアント側でUNIONします。準備されたステートメントが1つだけ必要です。ゆっくりと痛みを伴う。
  • 準備SELECT my_column FROM my_table WHERE search_column IN (?,?,?)して実行します。リストのサイズごとに1つの準備済みステートメントが必要です。速くて明白です。
  • 準備SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...して実行します。[またはUNION ALLそれらのセミコロンの代わりに使用します。--ed] size-of-IN-listごとに1つの準備済みステートメントが必要です。愚かなほど遅く、厳密にはよりも悪いWHERE search_column IN (?,?,?)ので、ブロガーがなぜそれを提案したのかさえわかりません。
  • ストアドプロシージャを使用して結果セットを作成します。
  • N個の異なるサイズリストのクエリを準備します。たとえば、2、10、50の値があるとします。6つの異なる値を持つINリストを検索するには、のようにサイズ10クエリを入力しますSELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6)。適切なサーバーは、クエリを実行する前に重複する値を最適化します。

ただし、これらのオプションはどれも優れたものではありません。

これらの場所では、同様に正気な代替案で重複する質問に答えていますが、それでも非常に優れた質問はありません。

正しい答えは、JDBC4とをサポートするサーバーx = ANY(y)を使用PreparedStatement.setArrayしている場合は、次の説明に従って使用することです。

setArrayただし、INリストを使用する方法はないようです。


時々、SQLステートメントは実行時にロードされます(たとえば、プロパティファイルから)が、可変数のパラメーターを必要とします。このような場合は、最初にクエリを定義します。

query=SELECT * FROM table t WHERE t.column IN (?)

次に、クエリを読み込みます。次に、実行する前にパラメーターの数を決定します。パラメータ数がわかったら、次を実行します。

sql = any( sql, count );

例えば:

/**
 * Converts a SQL statement containing exactly one IN clause to an IN clause
 * using multiple comma-delimited parameters.
 *
 * @param sql The SQL statement string with one IN clause.
 * @param params The number of parameters the SQL statement requires.
 * @return The SQL statement with (?) replaced with multiple parameter
 * placeholders.
 */
public static String any(String sql, final int params) {
    // Create a comma-delimited list based on the number of parameters.
    final StringBuilder sb = new StringBuilder(
            new String(new char[params]).replace("\0", "?,")
    );

    // Remove trailing comma.
    sb.setLength(Math.max(sb.length() - 1, 0));

    // For more than 1 parameter, replace the single parameter with
    // multiple parameter placeholders.
    if (sb.length() > 1) {
        sql = sql.replace("(?)", "(" + sb + ")");
    }

    // Return the modified comma-delimited list of parameters.
    return sql;
}

JDBC 4仕様を介した配列の受け渡しがサポートされていない特定のデータベースでは、このメソッドを使用すると、スロー= ?を高速なIN (?)句の条件に変換しやすくなり、anyメソッドを呼び出すことで拡張できます。


123

PostgreSQLのソリューション:

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table where search_column = ANY (?)"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));
final ResultSet rs = statement.executeQuery();
try {
    while(rs.next()) {
        // do some...
    }
} finally {
    rs.close();
}

または

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table " + 
        "where search_column IN (SELECT * FROM unnest(?))"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));
final ResultSet rs = statement.executeQuery();
try {
    while(rs.next()) {
        // do some...
    }
} finally {
    rs.close();
}

1
いいね。このコードのどの部分がPostreSQL固有ですか?「where search_column = ANY(?)」?またはconnection.createArrayOf?または、他の何か?
David Portabella

1
.createArrayOf()一部のため、PostgreSQL固有よりもJDBC4固有であると思いますが、ユーザーの厳密なセマンティクスArrayがJDBC仕様で定義されているかどうかはわかりません。
lvella 2012

3
.createArrayOf動作しない場合は、独自の配列リテラルを手動で作成できますString arrayLiteral = "{A,\"B \", C,D}" (「B」にはスペースがあり、Cにはスペースがないことに注意)。そしてstatement.setString(1,arrayLiteral)、準備されたステートメントは... IN (SELECT UNNEST(?::VARCHAR[]))or ... IN (SELECT UNNEST(CAST(? AS VARCHAR[])))です。(PS:でANY動作するとは思わないSELECT。)
ADTC 2013

素晴らしい解決策!本当に私のためにその日を救った。整数配列の場合、createArrayOf()の最初のパラメーターで「int」を使用しましたが、見栄えが良いです。ただし、ドキュメントに基づいて、最初のパラメーターはDB固有のように見えます。
Emmanuel Touzery 2013

2
これは最もクリーンなソリューションのようです。誰かがHSQLDB固有の構文を探している場合:IN(UNNEST(?))でこれを動作させることができました
aureianimus

19

簡単な方法はありません。ターゲットがステートメントキャッシュ率を高く保つことである場合(つまり、すべてのパラメーターカウントごとにステートメントを作成しないこと)、次の操作を実行できます。

  1. いくつかの(例:10)パラメータを使用してステートメントを作成します。

    ... WHERE A IN(?、?、?、?、?、?、?、?、?、?)...

  2. すべての実際のパラメーターをバインドする

    setString(1、 "foo"); setString(2、 "bar");

  3. 残りをNULLとしてバインドする

    setNull(3、Types.VARCHAR)... setNull(10、Types.VARCHAR)

NULLは何にも一致しないため、SQLプランビルダーによって最適化されます。

リストをDAO関数に渡すと、ロジックを簡単に自動化できます。

while( i < param.size() ) {
  ps.setString(i+1,param.get(i));
  i++;
}

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}

「NULLは決して一致しません」— NULLクエリでNULLデータベース内の値と一致しますか?
Craig McQueen

5
@CraigMcQueenいいえ、そうではありません。ANSI標準によれば、ヌルはヌルとさえ一致しません。
Dawood ibnカリーム2014

IS NULLキーワードを使用してNULLを一致させることができます。結合されたテーブルに存在しない行を検出する良い方法は、IS NULLとともにLEFT JOINを使用することです。これは表Bの一致を持たない表Aのすべての行を表示します「を選択a.URLは、TABLE_A FROM b.URL左はb.URLがNULL IS ON a_A.URL = b_B.URL bはTABLE_BをJOIN」
イェンスタンスタッド2014年

3
ただし、これには注意してください。 nullを同じ方法で処理しないNOT ININください。これを実行し、何が起こるかを確認します。select 'Matched' as did_it_match where 1 not in (5, null); 次にを削除してnull、魔法を監視します。
Brandon

または、すべての追加パラメーターを以前のパラメーターの値に設定できます。まともなDBエンジンはそれらをフィルターで除外します。だから、a IN (1,2,3,3,3,3,3)同じですa IN (1,2,3)。それNOT INは違って動作しa NOT IN (1,2,3,null,null,null,null)ます(any_value != NULL常にfalseであるので、常に行を返しません)。
Ruslan Stelmachenko

11

不快な回避策ですが、ネストされたクエリを使用することは確かに可能です。列を含む一時テーブルMYVALUESを作成します。MYVALUESテーブルに値のリストを挿入します。次に実行します

select my_column from my_table where search_column in ( SELECT value FROM MYVALUES )

醜いが、値のリストが非常に大きい場合に実行可能な代替手段。

この手法には、データベースが準備されたステートメントをキャッシュしない場合、オプティマイザーからのクエリプラン(ページで複数の値をチェックする、テーブルスキャンを値ごとに1回だけにするなど)が潜在的に改善されるという追加の利点があります。「INSERTS」はバッチで実行する必要があり、MYVALUESテーブルを微調整して、ロックやその他のオーバーヘッドの高い保護を最小限に抑える必要がある場合があります。


my_tableに対して一度に1つの値をクエリするよりも、どのような利点がありますか?
ポールトンブリン

3
クエリオプティマイザーは、読み込まれたページからすべての可能な一致を取得することにより、I / O負荷を軽減できます。テーブルスキャンまたはインデックススキャンは、値ごとに1回ではなく1回実行される場合があります。値を挿入するためのオーバーヘッドは、バッチ操作で削減でき、いくつかのクエリよりも少なくなる場合があります。
James Schek、2008年

1
見た目は良いですが、並行性に問題がある可能性があります。jdbc仕様には、メモリに一時的な匿名テーブルを作成する方法が含まれていますか?またはそのような何か、可能であればjdbcベンダー固有ではありませんか?
David Portabella

9

in()演算子の制限はすべての悪の根です。

これは些細な場合にも機能し、「準備済みステートメントの自動生成」で拡張できますが、常に制限があります。

  • 可変数のパラメーターを持つステートメントを作成している場合、呼び出しごとにSQL解析オーバーヘッドが発生します
  • 多くのプラットフォームでは、in()演算子のパラメーターの数が制限されています
  • すべてのプラットフォームで、SQLテキストの合計サイズが制限されているため、inパラメータの2000プレースホルダを送信できません。
  • JDBCドライバーに制限があるため、1000から10kのバインド変数を送信することはできません。

in()のアプローチは、場合によっては十分ですが、ロケットの証拠にはなりません:)

ロケットに対応するソリューションは、任意の数のパラメーターを個別の呼び出しで渡し(たとえば、パラメーターのCLOBを渡すことにより)、次にSQLでそれらを表すビュー(または他の方法)を使用して、whereで使用することです。基準。

ブルートフォースのバリアントはこちらhttp://tkyte.blogspot.hu/2006/06/varying-in-lists.html

ただし、PL / SQLを使用できる場合は、この混乱はかなりうまくいきます。

function getCustomers(in_customerIdList clob) return sys_refcursor is 
begin
    aux_in_list.parse(in_customerIdList);
    open res for
        select * 
        from   customer c,
               in_list v
        where  c.customer_id=v.token;
    return res;
end;

次に、パラメーターで任意の数のコンマ区切りの顧客IDを渡すことができます。

  • selectのSQLは安定しているため、解析遅延は発生しません
  • パイプライン化された関数の複雑さはありません-クエリは1つだけです
  • SQLはIN演算子の代わりに単純な結合を使用しており、非常に高速です。
  • すべての後に、それはの親指の良いルールではない、それは、Oracle、MySQLや類似したシンプルなデータベースエンジンよりも多くの申し出の光年であることから、任意のプレーンを選択またはDMLを使用してデータベースを打ちます。PL / SQLを使用すると、ストレージモデルをアプリケーションドメインモデルから効果的に隠すことができます。

ここでの秘訣は次のとおりです。

  • 長い文字列を受け入れ、dbセッションがアクセスできる場所に格納する呼び出しが必要です(たとえば、単純なパッケージ変数、またはdbms_session.set_context)。
  • 次に、これを行に解析できるビューが必要です
  • 次に、クエリを実行しているIDを含むビューがあるので、必要なのは、クエリされたテーブルへの単純な結合だけです。

ビューは次のようになります。

create or replace view in_list
as
select
    trim( substr (txt,
          instr (txt, ',', 1, level  ) + 1,
          instr (txt, ',', 1, level+1)
             - instr (txt, ',', 1, level) -1 ) ) as token
    from (select ','||aux_in_list.getpayload||',' txt from dual)
connect by level <= length(aux_in_list.getpayload)-length(replace(aux_in_list.getpayload,',',''))+1

ここで、aux_in_list.getpayloadは元の入力文字列を参照します。


可能なアプローチはpl / sql配列を渡すことです(Oracleのみでサポートされています)。ただし、それらを純粋なSQLで使用することはできないため、変換ステップが常に必要です。SQLで変換を行うことはできないため、結局のところ、すべてのパラメーターを含むCLOBを文字列で渡し、ビューで変換することが最も効率的なソリューションです。


6

これが私が自分のアプリケーションでそれを解決した方法です。理想的には、文字列に+を使用する代わりに、StringBuilderを使用する必要があります。

    String inParenthesis = "(?";
    for(int i = 1;i < myList.size();i++) {
      inParenthesis += ", ?";
    }
    inParenthesis += ")";

    try(PreparedStatement statement = SQLite.connection.prepareStatement(
        String.format("UPDATE table SET value='WINNER' WHERE startTime=? AND name=? AND traderIdx=? AND someValue IN %s", inParenthesis))) {
      int x = 1;
      statement.setLong(x++, race.startTime);
      statement.setString(x++, race.name);
      statement.setInt(x++, traderIdx);

      for(String str : race.betFair.winners) {
        statement.setString(x++, str);
      }

      int effected = statement.executeUpdate();
    }

具体的な数値の代わりに上記のxのような変数を使用すると、後でクエリを変更する場合に非常に役立ちます。


同じことを試しましたが、Oracleでは機能しませんでした
Santosh Raviteja

5

私は試したことはありませんが、.setArray()はあなたが探していることをしますか?

更新:明らかにそうではありません。setArrayは、前のクエリから取得したARRAY列、またはARRAY列を持つサブクエリからのjava.sql.Arrayでのみ機能するようです。


4
すべてのデータベースで機能するわけではありませんが、「正しい」アプローチです。
skaffman 2008年

あなたはすべてのドライバーを意味します。一部のドライバーには、この数年前(最後の世紀?)の標準と同等の独自仕様があります。もう1つの方法は、値のバッチを一時テーブルに
まとめる

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/…Sunによると、アレイのコンテンツは[通常]サーバー側に残り、必要に応じてプルされます。PreparedStatement.setArray()は、クライアント側で新しい配列を作成するのではなく、以前のResultSetから配列を送り返すことができます。
Chris Mazzola、

5

私の回避策は:

create or replace type split_tbl as table of varchar(32767);
/

create or replace function split
(
  p_list varchar2,
  p_del varchar2 := ','
) return split_tbl pipelined
is
  l_idx    pls_integer;
  l_list    varchar2(32767) := p_list;
  l_value    varchar2(32767);
begin
  loop
    l_idx := instr(l_list,p_del);
    if l_idx > 0 then
      pipe row(substr(l_list,1,l_idx-1));
      l_list := substr(l_list,l_idx+length(p_del));
    else
      pipe row(l_list);
      exit;
    end if;
  end loop;
  return;
end split;
/

これで、1つの変数を使用して、テーブル内のいくつかの値を取得できます。

select * from table(split('one,two,three'))
  one
  two
  three

select * from TABLE1 where COL1 in (select * from table(split('value1,value2')))
  value1 AAA
  value2 BBB

したがって、準備されたステートメントは次のようになります。

  "select * from TABLE where COL in (select * from table(split(?)))"

よろしく、

ハビエルアイバニーズ


これはPL / SQLです。他のデータベースでは動作しません。この実装には、入力パラメーターの制限があります-全長は32k文字に制限されます-パイプライン関数の呼び出しはPL / SQLとOracleのSQLエンジン間のコンテキスト切り替えを行うため、パフォーマンスの制限もあります。
ジービー

3

(基本的な文字列操作を使用して)でクエリ文字列を生成PreparedStatementして、?し、リスト内のアイテム数と一致する数の。

もちろん、あなたがあなたがそれをしているならOR、あなたはクエリでチェインされた巨大なコードを生成することからほんの一歩です?が、クエリ文字列に正しい数がなければ、これを回避する方法が他にあるかわかりません。


別の数を送信したいので、私にとっては本当に解決策ではありませんか?私はpsを呼び出すたびに。しかし、私がそれを考えていなかったとは思わないでください。:P
Chris Mazzola、

4
もう1つのハック:多数のパラメータープレースホルダーを使用できます-保持する値の最長リストと同じ数だけ使用できます。値のリストが短い場合は、値を繰り返すことができます:... WHERE searchfield IN(? 、?、?、?、?、?、?、?)、次に値を提供:A、B、C、D、A、B、C、D
Bill Karwin

1
しかし、全体的に私はAdamのソリューションを支持します。SQLを動的に生成し、連結しますか?渡す必要がある値の数と一致するプレースホルダー。
ビルカーウィン、

ビル、そのソリューションは、PreparedStatementを再利用したくない場合に有効です。別の解決策は、単一のparam呼び出しを複数回行い、クライアント側で結果を蓄積することです。おそらく、カスタム番号?で新しいステートメントを作成/実行する方が効率的です。でも毎回。
Chris Mazzola、

3

このjavadocに記載されているように、setArrayメソッドを使用できます。

PreparedStatement statement = connection.prepareStatement("Select * from emp where field in (?)");
Array array = statement.getConnection().createArrayOf("VARCHAR", new Object[]{"E1", "E2","E3"});
statement.setArray(1, array);
ResultSet rs = statement.executeQuery();

2
これはすべてのドライバーでサポートされているわけではありません。機能がサポートされていない場合、SQLFeatureNotSupportedException
名前のない

残念ながら、私のドライバーはそれをサポートしていません
EdXX

これはOracleでは機能しません
Santosh Raviteja

3

を使用Collections.nCopiesして、プレースホルダのコレクションを生成し、次を使用してそれらを結合できますString.join

List<String> params = getParams();
String placeHolders = String.join(",", Collections.nCopies(params.size(), "?"));
String sql = "select * from your_table where some_column in (" + placeHolders + ")";
try (   Connection connection = getConnection();
        PreparedStatement ps = connection.prepareStatement(sql)) {
    int i = 1;
    for (String param : params) {
        ps.setString(i++, param);
    }
    /*
     * Execute query/do stuff
     */
}

Oracle JDBCを使用する場合、これまでのところ最高のソリューションのようです...
jansohn

2

以下に、Javaでの準備済みステートメントを作成するための完全なソリューションを示します。

/*usage:

Util u = new Util(500); //500 items per bracket. 
String sqlBefore  = "select * from myTable where (";
List<Integer> values = new ArrayList<Integer>(Arrays.asList(1,2,4,5)); 
string sqlAfter = ") and foo = 'bar'"; 

PreparedStatement ps = u.prepareStatements(sqlBefore, values, sqlAfter, connection, "someId");
*/



import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Util {

    private int numValuesInClause;

    public Util(int numValuesInClause) {
        super();
        this.numValuesInClause = numValuesInClause;
    }

    public int getNumValuesInClause() {
        return numValuesInClause;
    }

    public void setNumValuesInClause(int numValuesInClause) {
        this.numValuesInClause = numValuesInClause;
    }

    /** Split a given list into a list of lists for the given size of numValuesInClause*/
    public List<List<Integer>> splitList(
            List<Integer> values) {


        List<List<Integer>> newList = new ArrayList<List<Integer>>(); 
        while (values.size() > numValuesInClause) {
            List<Integer> sublist = values.subList(0,numValuesInClause);
            List<Integer> values2 = values.subList(numValuesInClause, values.size());   
            values = values2; 

            newList.add( sublist);
        }
        newList.add(values);

        return newList;
    }

    /**
     * Generates a series of split out in clause statements. 
     * @param sqlBefore ""select * from dual where ("
     * @param values [1,2,3,4,5,6,7,8,9,10]
     * @param "sqlAfter ) and id = 5"
     * @return "select * from dual where (id in (1,2,3) or id in (4,5,6) or id in (7,8,9) or id in (10)"
     */
    public String genInClauseSql(String sqlBefore, List<Integer> values,
            String sqlAfter, String identifier) 
    {
        List<List<Integer>> newLists = splitList(values);
        String stmt = sqlBefore;

        /* now generate the in clause for each list */
        int j = 0; /* keep track of list:newLists index */
        for (List<Integer> list : newLists) {
            stmt = stmt + identifier +" in (";
            StringBuilder innerBuilder = new StringBuilder();

            for (int i = 0; i < list.size(); i++) {
                innerBuilder.append("?,");
            }



            String inClause = innerBuilder.deleteCharAt(
                    innerBuilder.length() - 1).toString();

            stmt = stmt + inClause;
            stmt = stmt + ")";


            if (++j < newLists.size()) {
                stmt = stmt + " OR ";
            }

        }

        stmt = stmt + sqlAfter;
        return stmt;
    }

    /**
     * Method to convert your SQL and a list of ID into a safe prepared
     * statements
     * 
     * @throws SQLException
     */
    public PreparedStatement prepareStatements(String sqlBefore,
            ArrayList<Integer> values, String sqlAfter, Connection c, String identifier)
            throws SQLException {

        /* First split our potentially big list into lots of lists */
        String stmt = genInClauseSql(sqlBefore, values, sqlAfter, identifier);
        PreparedStatement ps = c.prepareStatement(stmt);

        int i = 1;
        for (int val : values)
        {

            ps.setInt(i++, val);

        }
        return ps;

    }

}

2

Springでは、java.util.ListsをNamedParameterJdbcTemplateに渡すことができます。にます。これにより、引数の数に応じて(?、?、?、...、?)の生成が自動化されます。

Oracleの場合、このブログ投稿ではoracle.sql.ARRAYの使用について説明しています(Connection.createArrayOfはOracleでは機能しません)。このためには、SQLステートメントを変更する必要があります。

SELECT my_column FROM my_table where search_column IN (select COLUMN_VALUE from table(?))

Oracleテーブル関数は、の値を使用できるようにテーブルに渡された配列を変換INステートメント。


1

instr関数を使用してみますか?

select my_column from my_table where  instr(?, ','||search_column||',') > 0

その後

ps.setString(1, ",A,B,C,"); 

確かに、これはちょっと汚いハックですが、SQLインジェクションの機会を減らします。とにかくオラクルで動作します。


ああ、私はそれがインデックスを利用しないことを知っています
stjohnroe

たとえば、文字列に「、」が含まれている場合、一部の文字列では機能しません。
David Portabella

1

Sormulaは、java.util.Collectionオブジェクトをパラメーターとして指定できるようにすることで、SQL IN演算子をサポートしています。それは?各要素のコレクション。例4を参照してください(例の SQLは、何が作成されるかを明確にするためのコメントですが、Sormulaでは使用されていません)。


1

使用する代わりに

SELECT my_column FROM my_table where search_column IN (?)

次のようにSQLステートメントを使用する

select id, name from users where id in (?, ?, ?)

そして

preparedStatement.setString( 1, 'A');
preparedStatement.setString( 2,'B');
preparedStatement.setString( 3, 'C');

または、ストアドプロシージャを使用します。これは、SQLステートメントがデータベースサーバーにコンパイルおよび保存されるため、最良のソリューションです。


1

準備されたステートメントに関連するいくつかの制限に遭遇しました:

  1. 準備されたステートメントは同じセッション(Postgres)内でのみキャッシュされるため、実際には接続プールでのみ機能します
  2. @BalusCによって提案されたさまざまな準備されたステートメントの多くにより、キャッシュがいっぱいになり、以前にキャッシュされたステートメントが削除されます。
  3. クエリは最適化され、インデックスを使用する必要があります。当たり前のように聞こえますが、たとえば、@ Borisが提案したANY(ARRAY ...)ステートメントは、トップの回答の1つでインデックスを使用できず、キャッシュにもかかわらずクエリが遅くなります
  4. 準備されたステートメントはクエリプランもキャッシュし、ステートメントで指定されたパラメーターの実際の値は使用できません。

提案されたソリューションの中から、クエリのパフォーマンスを低下させず、クエリの数を少なくするソリューションを選択します。これは、@ Donリンクからの#4(いくつかのクエリのバッチ処理)または不要な '?'にNULL値を指定することになります。@Vladimir Dyuzhevによって提案されたマーク


1

私はこれのためにPostgreSQL固有のオプションを考え出しました。これは少しハックであり、独自の長所と短所と制限がありますが、機能するようで、特定の開発言語、プラットフォーム、またはPGドライバーに限定されません。

もちろん、秘訣は、任意の長さの値のコレクションを単一のパラメーターとして渡し、dbにそれを複数の値として認識させる方法を見つけることです。私が取り組んでいる解決策は、コレクション内の値から区切られた文字列を作成し、その文字列を単一のパラメーターとして渡し、PostgreSQLに必要なキャストでstring_to_array()を使用して適切に使用することです。

したがって、「foo」、「blah」、「abc」を検索する場合は、「foo、blah、abc」のように、それらを1つの文字列に連結することができます。これが単純なSQLです。

select column from table
where search_column = any (string_to_array('foo,blah,abc', ',')::text[]);

明示的なキャストを、結果の値の配列にしたいもの(int、text、uuidなど)に明らかに変更します。また、関数は単一の文字列値(または、区切り文字をカスタマイズする場合は2つ)を取るので同様に)、それを準備されたステートメントのパラメーターとして渡すことができます:

select column from table
where search_column = any (string_to_array($1, ',')::text[]);

これは、LIKE比較のようなものをサポートするのに十分なほど柔軟です。

select column from table
where search_column like any (string_to_array('foo%,blah%,abc%', ',')::text[]);

繰り返しますが、これはハックですが、機能し、*コンパイル済み*のプリコンパイル済みステートメントを引き続き使用できます。個別のパラメーターを、付随するセキュリティと(おそらく)パフォーマンス上の利点があります。それは賢明で、実際にパフォーマンスがありますか?当然ですが、クエリが実行される前に文字列の解析とキャストが行われる可能性があるためです。3、5、数十の値を送信することを期待している場合、確かにそれはおそらく問題ありません。数千?ええ、多分それほどではないでしょう。YMMV、制限および除外が適用され、明示または黙示の保証はありません。

しかし、それは機能します。


0

完全を期すために:値のセットが大きすぎない限り、次のようなステートメントを単純に文字列構成することできます。

... WHERE tab.col = ? OR tab.col = ? OR tab.col = ?

これをprepare()に渡し、ループでsetXXX()を使用してすべての値を設定できます。これは不自然に見えますが、多くの「大きな」商用システムは、Oracleのステートメントに対して32 KB(そうだと思います)などのDB固有の制限に達するまで、この種のことを日常的に行っています。

もちろん、セットが不当に大きくならないようにする必要があります。そうでない場合は、エラートラップを実行する必要があります。


はい、あなたが正しい。この場合の目標は、PreparedStatementを毎回異なる数のアイテムで再利用することでした。
Chris Mazzola、

3
「OR」を使用すると、意図がわかりにくくなります。読みやすく、意図がより明確であるため、「IN」を使用します。切り替える唯一の理由は、クエリプランが異なる場合です。
James Schek、2008年

0

アダムのアイデアに従う。準備されたステートメントをmy_tableからselect my_columnのようなものにします(#)のsearch_columnを作成します文字列xを作成し、それに「?、?、?」の数を入力します 値のリストに応じて、新しいString x a Populateのクエリの#を変更するだけです


0

リスト内の項目の数と一致する?の数が含まれるように、PreparedStatementでクエリ文字列を生成します。次に例を示します。

public void myQuery(List<String> items, int other) {
  ...
  String q4in = generateQsForIn(items.size());
  String sql = "select * from stuff where foo in ( " + q4in + " ) and bar = ?";
  PreparedStatement ps = connection.prepareStatement(sql);
  int i = 1;
  for (String item : items) {
    ps.setString(i++, item);
  }
  ps.setInt(i++, other);
  ResultSet rs = ps.executeQuery();
  ...
}

private String generateQsForIn(int numQs) {
    String items = "";
    for (int i = 0; i < numQs; i++) {
        if (i != 0) items += ", ";
        items += "?";
    }
    return items;
}

5
StringBuilderを使用する必要はもうありません。コンパイラは+記号をとにかくStringBuilder.append()に変換するので、パフォーマンスに影響はありません。自分で試してください:)
neu242 2009

5
@ neu242:はい、コンパイラはを使用しStringBuilderます。しかし、あなたが考える方法ではありません。逆コンパイルgenerateQsForInすると、ループの反復ごとに2つの new StringBuilderが割り当てられ、toStringそれぞれに呼び出されることがわかります。StringBuilder最適化は唯一のようなものをキャッチする"x" + i+ "y" + jが、1つの表現を超えて拡張しません。
AH、

@ neu242 ps.setObject(1,items)リストを繰り返し処理してからparamteres?を設定する代わりに、使用できませんか?
Neha Choudhary 2013

0

PreparedStatementのIN句に使用できる別の方法があります。

  1. 単一クエリの使用-パフォーマンスが最も低く、リソースを大量に消費します
  2. StoredProcedureの使用-最速ですがデータベース固有
  3. PreparedStatementの動的クエリの作成-良好なパフォーマンスが得られますが、キャッシュのメリットは得られず、PreparedStatementは毎回再コンパイルされます。
  4. PreparedStatementクエリでのNULLの使用-最適なパフォーマンス。IN句の引数の制限がわかっている場合に最適です。制限がない場合は、クエリをバッチで実行できます。サンプルコードスニペットは次のとおりです。

        int i = 1;
        for(; i <=ids.length; i++){
            ps.setInt(i, ids[i-1]);
        }
    
        //set null for remaining ones
        for(; i<=PARAM_SIZE;i++){
            ps.setNull(i, java.sql.Types.INTEGER);
        }

これらの代替アプローチの詳細については、こちらで確認できます


「PreparedStatementの動的クエリの作成-良好なパフォーマンスが得られますが、キャッシュの利点が得られず、PreparedStatementは毎回再コンパイルされます。」キャッシュし、再コンパイルを回避することで、準備されたステートメントのパフォーマンスが向上します。したがって、私はあなたの主張に同意しません。ただし、連結/動的入力をコンマに制限しているため、SQLインジェクションは防止されます。
ブランドン

私はあなたに同意しますが、ここでの「良好なパフォーマンス」はこの特定のシナリオのためのものです。アプローチ1よりもパフォーマンスは優れていますが、アプローチ2が最も高速です。
Pankaj 14

0

状況によっては、正規表現が役立つ場合があります。Oracleで確認した例は次のとおりです。

select * from my_table where REGEXP_LIKE (search_column, 'value1|value2')

しかし、それにはいくつかの欠点があります。

  1. 適用した列は、少なくとも暗黙的にvarchar / charに変換する必要があります。
  2. 特殊文字には注意が必要です。
  3. パフォーマンスが低下する可能性があります。私の場合、INバージョンはインデックスと範囲スキャンを使用し、REGEXPバージョンはフルスキャンを実行します。

0

さまざまなフォーラムでさまざまな解決策を検討しても良い解決策が見つからなかった後、私が思いついた以下のハックは、追跡およびコーディングが最も簡単だと感じています。

例: 'IN'句で渡す複数のパラメーターがあるとします。「IN」句の中にダミーの文字列を挿入するだけです。たとえば、「PARAM」は、このダミーの文字列の代わりに来るパラメータのリストを示します。

    select * from TABLE_A where ATTR IN (PARAM);

Javaコードでは、すべてのパラメーターを1つのString変数に収集できます。これは次のように実行できます。

    String param1 = "X";
    String param2 = "Y";
    String param1 = param1.append(",").append(param2);

この場合は、すべてのパラメーターをコンマで区切って単一のストリング変数「param1」に追加できます。

すべてのパラメータを1つの文字列に収集した後、クエリ内のダミーテキスト(この場合は「PARAM」)をパラメータ文字列、つまりparam1に置き換えるだけです。これはあなたがする必要があることです:

    String query = query.replaceFirst("PARAM",param1); where we have the value of query as 

    query = "select * from TABLE_A where ATTR IN (PARAM)";

これで、executeQuery()メソッドを使用してクエリを実行できます。クエリのどこにも「PARAM」という単語が含まれていないことを確認してください。「PARAM」という単語の代わりに特殊文字とアルファベットの組み合わせを使用して、そのような単語がクエリに含まれる可能性がないことを確認できます。あなたが解決策を得たことを願っています。

注:これは準備済みのクエリではありませんが、コードで実行したい作業を実行します。


0

完全を期すために、そして他の誰かがそれを提案するのを見なかったので

上記の複雑な提案を実装する前に、SQLインジェクションが実際にシナリオの問題であるかどうかを検討してください。

多くの場合、IN(...)に提供される値は、インジェクションが不可能であることを確認できる方法で生成されたIDのリストです(例:some_tableからの以前のselect some_idの結果some_condition。)

その場合は、この値を連結するだけで、サービスや準備されたステートメントを使用したり、このクエリの他のパラメーターに使用したりしないでください。

query="select f1,f2 from t1 where f3=? and f2 in (" + sListOfIds + ");";

0

PreparedStatementは、SQL IN句を処理するための適切な方法を提供していません。パーhttp://www.javaranch.com/journal/200510/Journal200510.jsp#a2「あなたは、SQL文の一部になることを意味しているものを代用することはできません。SQL自体は変更することができますので、この場合は必要であり、ドライバはステートメントをプリコンパイルできません。また、SQLインジェクション攻撃を防ぐというすばらしい副作用もあります。」私は次のアプローチを使用してしまいました:

String query = "SELECT my_column FROM my_table where search_column IN ($searchColumns)";
query = query.replace("$searchColumns", "'A', 'B', 'C'");
Statement stmt = connection.createStatement();
boolean hasResults = stmt.execute(query);
do {
    if (hasResults)
        return stmt.getResultSet();

    hasResults = stmt.getMoreResults();

} while (hasResults || stmt.getUpdateCount() != -1);

0

SetArrayは最良のソリューションですが、多くの古いドライバーでは使用できません。次の回避策はjava8で使用できます。

String baseQuery ="SELECT my_column FROM my_table where search_column IN (%s)"

String markersString = inputArray.stream().map(e -> "?").collect(joining(","));
String sqlQuery = String.format(baseSQL, markersString);

//Now create Prepared Statement and use loop to Set entries
int index=1;

for (String input : inputArray) {
     preparedStatement.setString(index++, input);
}

このソリューションは、クエリ文字列が手動の反復によって構築される他の醜いwhileループソリューションよりも優れています


0

これは私のために働きました(疑似コード):

public class SqlHelper
{
    public static final ArrayList<String>platformList = new ArrayList<>(Arrays.asList("iOS","Android","Windows","Mac"));

    public static final String testQuery = "select * from devices where platform_nm in (:PLATFORM_NAME)";
}

バインドを指定:

public class Test extends NamedParameterJdbcDaoSupport
public List<SampleModelClass> runQuery()
{
    //define rowMapper to insert in object of SampleClass
    final Map<String,Object> map = new HashMap<>();
    map.put("PLATFORM_LIST",DeviceDataSyncQueryConstants.platformList);
    return getNamedParameterJdbcTemplate().query(SqlHelper.testQuery, map, rowMapper)
}

0

SQLiteおよびOracleデータベースの私の例。

最初のForループは、PreparedStatementオブジェクトを作成するためのものです。

2番目のForループは、PreparedStatementパラメータの値を提供するためのものです。

List<String> roles = Arrays.asList("role1","role2","role3");
Map<String, String> menu = getMenu(conn, roles);

public static Map<String, String> getMenu(Connection conn, List<String> roles ) {
    Map<String, String> menu = new LinkedHashMap<String, String>();

    PreparedStatement stmt;
    ResultSet rset;
    String sql;
    try {
        if (roles == null) {
            throw new Exception();
        }
        int size = roles.size();
        if (size == 0) {
            throw new Exception("empty list");
        }
        StringBuilder sb = new StringBuilder();
        sb.append("select page_controller, page_name from pages "
                + " where page_controller in (");
        for (int i = 0; i < size; i++) {
            sb.append("?,");
        }
        sb.setLength(sb.length() - 1);
        sb.append(") order by page_id");
        sql = sb.toString();
        stmt = conn.prepareStatement(sql);
        for (int i = 0; i < size; i++) {
            stmt.setString(i + 1, roles.get(i));
        }
        rset = stmt.executeQuery();
        while (rset.next()) {
            menu.put(rset.getString(1), rset.getString(2));
        }

        conn.close();
    } catch (Exception ex) {
        logger.info(ex.toString());
        try {
            conn.close();
        } catch (SQLException e) {
        }
        return menu;
    }
    return menu;
}

-3

私の回避策(JavaScript)

    var s1 = " SELECT "

 + "FROM   table t "

 + "  where t.field in ";

  var s3 = '(';

  for(var i =0;i<searchTerms.length;i++)
  {
    if(i+1 == searchTerms.length)
    {
     s3  = s3+'?)';
    }
    else
    {
        s3  = s3+'?, ' ;
    }
   }
    var query = s1+s3;

    var pstmt = connection.prepareStatement(query);

     for(var i =0;i<searchTerms.length;i++)
    {
        pstmt.setString(i+1, searchTerms[i]);
    }

SearchTerms 入力/キー/フィールドなどを含む配列です

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.