PreparedStatement IN-Klauselalternativen?

342

Was sind die besten Problemumgehungen für die Verwendung einer SQL- INKlausel mit Instanzen von java.sql.PreparedStatement, die aufgrund von Sicherheitsproblemen bei SQL-Injection-Angriffen nicht für mehrere Werte unterstützt wird: Ein ?Platzhalter steht für einen Wert und nicht für eine Liste von Werten.

Betrachten Sie die folgende SQL-Anweisung:

SELECT my_column FROM my_table where search_column IN (?)

Die Verwendung preparedStatement.setString( 1, "'A', 'B', 'C'" );ist im Wesentlichen ein nicht funktionierender Versuch, die Gründe für ?die erstmalige Verwendung zu umgehen.

Welche Problemumgehungen sind verfügbar?

Chris Mazzola
quelle
1
Oscar, ich denke, die dynamische Generierung von (?,?, ....) ist die einfachste Problemumgehung, wenn Sie eine IN-Klausel benötigen, aber ich habe sie einzelnen Aufrufen überlassen, da die Leistung in meinem speziellen Fall ausreichend war.
Chris Mazzola
6
Einer der Vorteile vorbereiteter Aussagen besteht darin, dass sohuld aus Effizienzgründen einmal kompiliert werden kann. Indem die in-Klausel dynamisiert wird, wird die vorbereitete Anweisung effektiv negiert.
2
Tatsächlich funktioniert dies für MySQL (mit setObject wird ein Array von String als Parameterwert festgelegt). Welche Datenbank verwenden Sie?
Frans
Hier ist eine Oracle-spezifische Antwort
Peter Hart
Hier ist eine verwandte Frage: stackoverflow.com/q/6956025/521799
Lukas Eder

Antworten:

194

Eine Analyse der verschiedenen verfügbaren Optionen sowie der Vor- und Nachteile der einzelnen Optionen finden Sie hier .

Die vorgeschlagenen Optionen sind:

  • Bereiten Sie es vor SELECT my_column FROM my_table WHERE search_column = ?, führen Sie es für jeden Wert aus und UNION die Ergebnisse clientseitig. Benötigt nur eine vorbereitete Anweisung. Langsam und schmerzhaft.
  • Bereiten Sie es vor SELECT my_column FROM my_table WHERE search_column IN (?,?,?)und führen Sie es aus. Erfordert eine vorbereitete Anweisung pro Größe der IN-Liste. Schnell und offensichtlich.
  • Bereiten Sie es vor SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...und führen Sie es aus. [Oder UNION ALLanstelle dieser Semikolons verwenden. --ed] Erfordert eine vorbereitete Anweisung pro Größe der IN-Liste. Dumm langsam, streng schlimmer als WHERE search_column IN (?,?,?), also weiß ich nicht, warum der Blogger es überhaupt vorgeschlagen hat.
  • Verwenden Sie eine gespeicherte Prozedur, um die Ergebnismenge zu erstellen.
  • Bereiten Sie N Abfragen mit unterschiedlicher Größe der IN-Liste vor. sagen wir mit 2, 10 und 50 Werten. Um nach einer IN-Liste mit 6 verschiedenen Werten zu suchen, füllen Sie die Abfrage der Größe 10 so aus, dass sie aussieht SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Jeder anständige Server optimiert die doppelten Werte, bevor die Abfrage ausgeführt wird.

Keine dieser Optionen ist jedoch super toll.

An diesen Stellen wurden doppelte Fragen mit ebenso vernünftigen Alternativen beantwortet, von denen immer noch keine besonders gut ist:

Die richtige Antwort, wenn Sie JDBC4 und einen Server verwenden, der dies unterstützt x = ANY(y), lautet wie folgt PreparedStatement.setArray:

Es scheint jedoch keine Möglichkeit zu geben setArray, mit IN-Listen zu arbeiten.


Manchmal werden SQL-Anweisungen zur Laufzeit geladen (z. B. aus einer Eigenschaftendatei), erfordern jedoch eine variable Anzahl von Parametern. In solchen Fällen definieren Sie zuerst die Abfrage:

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

Laden Sie als Nächstes die Abfrage. Bestimmen Sie dann die Anzahl der Parameter, bevor Sie sie ausführen. Sobald die Parameteranzahl bekannt ist, führen Sie Folgendes aus:

sql = any( sql, count );

Zum Beispiel:

/**
 * 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;
}

Bei bestimmten Datenbanken, bei denen die Übergabe eines Arrays über die JDBC 4-Spezifikation nicht unterstützt wird, kann diese Methode die Umwandlung der langsamen = ?in die schnellere IN (?)Klauselbedingung erleichtern , die dann durch Aufrufen der anyMethode erweitert werden kann.

Dónal
quelle
123

Lösung für 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();
}

oder

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();
}
Boris
quelle
1
sieht gut aus. Welcher Teil dieses Codes ist PostreSQL-spezifisch? das "wo search_column = ANY (?)"? oder die connection.createArrayOf? oder etwas anderes?
David Portabella
1
Ich denke, es ist aufgrund des .createArrayOf()Teils mehr JDBC4-spezifisch als PostgreSQL-spezifisch, aber ich bin nicht sicher, ob die strenge Semantik für Benutzer Arraydurch die JDBC-Spezifikation definiert ist.
lvella
3
Wenn .createArrayOfnicht funktioniert, können Sie Ihre eigene manuelle Erstellung von Arrayliteral wie tun String arrayLiteral = "{A,\"B \", C,D}" (beachten Sie, dass „B“ einen Raum hat , während C nicht) und dann , statement.setString(1,arrayLiteral)wenn die vorbereitete Anweisung ist ... IN (SELECT UNNEST(?::VARCHAR[]))oder ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: Ich glaube nicht, dass es ANYmit a funktioniert SELECT.)
ADTC
Tolle Lösung! Wirklich den Tag für mich gerettet. Für ein ganzzahliges Array habe ich "int" im ersten Parameter von createArrayOf () verwendet und es sieht gut aus. Dieser erste Parameter erscheint jedoch DB-spezifisch, basierend auf der Dokumentation.
Emmanuel Touzery
2
Dies scheint die sauberste Lösung zu sein. Wenn jemand nach der HSQLDB-spezifischen Syntax sucht: Ich habe es geschafft, dass dies mit IN (UNNEST (?))
aureianimus
19

Kein einfacher Weg AFAIK. Wenn das Ziel darin besteht, das Verhältnis des Anweisungscaches hoch zu halten (dh nicht für jede Parameteranzahl eine Anweisung zu erstellen), können Sie Folgendes tun:

  1. Erstellen Sie eine Anweisung mit einigen (z. B. 10) Parametern:

    ... WO EIN IN (?,?,?,?,?,?,?,?,?,?) ...

  2. Binden Sie alle aktuellen Parameter

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

  3. Binden Sie den Rest als NULL

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

NULL stimmt nie mit etwas überein, daher wird es vom SQL Plan Builder optimiert.

Die Logik ist einfach zu automatisieren, wenn Sie eine Liste an eine DAO-Funktion übergeben:

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

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}
Vladimir Dyuzhev
quelle
"NULL stimmt nie mit etwas überein" - Würde NULLdie Abfrage mit einem NULLWert in der Datenbank übereinstimmen ?
Craig McQueen
5
@CraigMcQueen Nein, würde es nicht. Nach dem ANSI-Standard stimmt Null nicht einmal mit Null überein.
Dawood ibn Kareem
Sie können NULL mit dem Schlüsselwort IS NULL abgleichen. Eine gute Möglichkeit, Zeilen zu erkennen, die in der verknüpften Tabelle nicht vorhanden sind, besteht darin, LEFT JOIN zusammen mit IS NULL zu verwenden. 'SELECT a.URL, b.URL FROM TABLE_A a LEFT JOIN TABLE_B b ON a_A.URL = b_B.URL WHERE b.URL NULL' Dies zeigt alle Zeilen in Tabelle A an, die in Tabelle B nicht übereinstimmen.
Jens Tandstad
3
Sei aber vorsichtig damit. NOT INund INbehandeln Sie Nullen nicht auf die gleiche Weise. Führen Sie dies aus und sehen Sie, was passiert: select 'Matched' as did_it_match where 1 not in (5, null); Entfernen Sie dann die nullund beobachten Sie die Magie.
Brandon
Oder Sie können alle zusätzlichen Parameter auf den Wert eines vorherigen Parameters setzen. Jede anständige DB-Engine filtert sie heraus. So a IN (1,2,3,3,3,3,3)ist das gleiche wie a IN (1,2,3). Es funktioniert auch mit Im NOT INGegensatz a NOT IN (1,2,3,null,null,null,null)(was immer keine Zeilen zurückgibt, da any_value != NULLes immer falsch ist).
Ruslan Stelmachenko
11

Eine unangenehme Umgehung, aber durchaus machbar, ist die Verwendung einer verschachtelten Abfrage. Erstellen Sie eine temporäre Tabelle MYVALUES mit einer Spalte darin. Fügen Sie Ihre Werteliste in die Tabelle MYVALUES ein. Dann ausführen

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

Hässlich, aber eine praktikable Alternative, wenn Ihre Werteliste sehr groß ist.

Diese Technik hat den zusätzlichen Vorteil, dass potenziell bessere Abfragepläne vom Optimierer (eine Seite auf mehrere Werte prüfen, Tabellen nur einmal statt einmal pro Wert usw. scannen) Overhead sparen können, wenn Ihre Datenbank vorbereitete Anweisungen nicht zwischenspeichert. Ihre "INSERTS" müssten im Batch ausgeführt werden, und die MYVALUES-Tabelle muss möglicherweise optimiert werden, um minimale Sperren oder andere Schutzmaßnahmen mit hohem Overhead zu erzielen.

James Schek
quelle
Welche Vorteile hätte das, wenn Sie my_table jeweils einen Wert abfragen würden?
Paul Tomblin
3
Das Abfrageoptimierungsprogramm kann die E / A-Belastung reduzieren, indem alle möglichen Übereinstimmungen von einer geladenen Seite abgerufen werden. Tabellen- oder Indexscans können einmal statt einmal pro Wert durchgeführt werden. Der Aufwand für das Einfügen von Werten kann durch Stapeloperationen reduziert werden und kann weniger als mehrere Abfragen betragen.
James Schek
1
es sieht gut aus, aber es könnte Probleme mit der Parallelität geben. Enthält die JDBC-Spezifikation eine Möglichkeit, eine zeitlich anonyme Tabelle im Speicher zu erstellen? oder so ähnlich, wenn möglich nicht jdbc-herstellerspezifisch?
David Portabella
9

Einschränkungen des in () -Operators sind die Wurzel allen Übels.

Es funktioniert für triviale Fälle, und Sie können es mit "automatischer Generierung der vorbereiteten Anweisung" erweitern, es hat jedoch immer seine Grenzen.

  • Wenn Sie eine Anweisung mit einer variablen Anzahl von Parametern erstellen, entsteht bei jedem Aufruf ein SQL-Analyse-Overhead
  • Auf vielen Plattformen ist die Anzahl der Parameter des Operators in () begrenzt
  • Auf allen Plattformen ist die gesamte SQL-Textgröße begrenzt, sodass keine 2000 Platzhalter für die in-Parameter gesendet werden können
  • Das Senden von Bindevariablen von 1000-10k ist nicht möglich, da der JDBC-Treiber seine Einschränkungen hat

Der in () -Ansatz kann in einigen Fällen gut genug sein, ist aber nicht raketenfest :)

Die raketenfeste Lösung besteht darin, die beliebige Anzahl von Parametern in einem separaten Aufruf zu übergeben (z. B. durch Übergeben eines Parameterclobs) und dann eine Ansicht (oder eine andere Möglichkeit) zu haben, um sie in SQL darzustellen und in Ihrem Where zu verwenden Kriterien.

Eine Brute-Force-Variante finden Sie hier http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Wenn Sie jedoch PL / SQL verwenden können, kann dieses Durcheinander ziemlich ordentlich werden.

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;

Anschließend können Sie eine beliebige Anzahl von durch Kommas getrennten Kunden-IDs im Parameter übergeben und:

  • wird keine Analyseverzögerung erhalten, da die SQL für select stabil ist
  • Keine Komplexität der Pipeline-Funktionen - es ist nur eine Abfrage
  • Die SQL verwendet einen einfachen Join anstelle eines IN-Operators, was ziemlich schnell ist
  • Schließlich ist es eine gute Faustregel gilt : der nicht auf die Datenbank mit jeder Ebene wählen oder DML schlagen, da es Oracle ist, die Angebote Lichtjahre von mehr als MySQL oder ähnlichem einfachen Datenbank - Engines. Mit PL / SQL können Sie das Speichermodell effektiv vor Ihrem Anwendungsdomänenmodell verbergen.

Der Trick hier ist:

  • Wir brauchen einen Aufruf, der die lange Zeichenfolge akzeptiert und irgendwo speichert, wo die DB-Sitzung darauf zugreifen kann (z. B. einfache Paketvariable oder dbms_session.set_context).
  • dann brauchen wir eine Ansicht, die dies in Zeilen analysieren kann
  • und dann haben Sie eine Ansicht, die die IDs enthält, die Sie abfragen. Alles, was Sie brauchen, ist eine einfache Verknüpfung mit der abgefragten Tabelle.

Die Ansicht sieht aus wie:

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

Dabei bezieht sich aux_in_list.getpayload auf die ursprüngliche Eingabezeichenfolge.


Ein möglicher Ansatz wäre das Übergeben von pl / sql-Arrays (nur von Oracle unterstützt). Sie können diese jedoch nicht in reinem SQL verwenden. Daher ist immer ein Konvertierungsschritt erforderlich. Die Konvertierung kann nicht in SQL durchgeführt werden. Daher ist es die effizienteste Lösung, einen Clob mit allen Parametern in der Zeichenfolge zu übergeben und in eine Ansicht zu konvertieren.

Gee Bee
quelle
6

So habe ich es in meiner eigenen Anwendung gelöst. Idealerweise sollten Sie einen StringBuilder verwenden, anstatt + für Strings zu verwenden.

    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();
    }

Die Verwendung einer Variablen wie x oben anstelle konkreter Zahlen hilft sehr, wenn Sie die Abfrage zu einem späteren Zeitpunkt ändern möchten.

m.sabouri
quelle
versuchte dasselbe, funktionierte nicht für Oracle
Santosh Raviteja
5

Ich habe es noch nie versucht, aber würde .setArray () das tun, wonach Sie suchen?

Update : Offensichtlich nicht. setArray scheint nur mit einem java.sql.Array zu funktionieren, das aus einer ARRAY-Spalte stammt, die Sie aus einer vorherigen Abfrage abgerufen haben, oder mit einer Unterabfrage mit einer ARRAY-Spalte.

Paul Tomblin
quelle
4
Funktioniert nicht mit allen Datenbanken, ist aber der "richtige" Ansatz.
Skaffman
Du meinst alle Fahrer. Einige Fahrer haben proprietäre Entsprechungen des diesjährigen Standards (letztes Jahrhundert?). Eine andere Möglichkeit besteht darin, einen Stapel von Werten in eine temporäre Tabelle zu
packen
java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Laut Sun verbleibt der Array-Inhalt [normalerweise] auf der Serverseite und wird nach Bedarf abgerufen. PreparedStatement.setArray () kann ein Array aus einem vorherigen ResultSet zurücksenden und kein neues Array auf der Clientseite erstellen.
Chris Mazzola
5

Meine Problemumgehung ist:

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;
/

Jetzt können Sie eine Variable verwenden, um einige Werte in einer Tabelle zu erhalten:

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

Die vorbereitete Aussage könnte also sein:

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

Grüße,

Javier Ibanez

Javier Ibanez
quelle
Das ist PL / SQL, ja. Es wird in anderen Datenbanken nicht funktionieren. Beachten Sie, dass diese Implementierung eine Einschränkung der Eingabeparameter aufweist - die Gesamtlänge ist auf 32.000 Zeichen begrenzt - sowie eine Leistungsbeschränkung, da der Aufruf der Pipeline-Funktion einen Kontextwechsel zwischen PL / SQL- und SQL-Engines von Oracle bewirkt.
Gee Bee
3

Ich nehme an, Sie könnten (mithilfe der grundlegenden Zeichenfolgenmanipulation) die Abfragezeichenfolge in generieren PreparedStatement, um eine ?Anzahl von Elementen zu erhalten, die mit der Anzahl der Elemente in Ihrer Liste übereinstimmt.

Wenn Sie dies tun, sind Sie natürlich nur einen Schritt davon entfernt, einen ORin Ihrer Abfrage verketteten Riesen zu generieren , aber ohne die richtige Anzahl von ?in der Abfragezeichenfolge zu haben, sehe ich nicht, wie Sie dies sonst umgehen können.

Adam Bellaire
quelle
Keine wirkliche Lösung für mich, da ich eine andere Anzahl von senden möchte? jedes mal rufe ich die ps an. Aber glaube nicht, dass ich nicht darüber nachgedacht hätte. : P
Chris Mazzola
4
Ein weiterer Hack: Sie können eine große Anzahl von Parameterplatzhaltern verwenden - so viele wie die längste Werteliste, die Sie haben - und wenn Ihre Werteliste kürzer ist, können Sie Werte wiederholen: ... WHERE Suchfeld IN (? ,?,?,?,?,?,?,?) und geben Sie dann Werte an: A, B, C, D, A, B, C, D
Bill Karwin
1
Aber insgesamt bevorzuge ich Adams Lösung: SQL dynamisch generieren und verketten? Platzhalter, die der Anzahl der Werte entsprechen, die Sie übergeben müssen.
Bill Karwin
Bill, diese Lösung funktioniert, wenn ich das PreparedStatement nicht wiederverwenden möchte. Eine andere Lösung besteht darin, den einzelnen Parameter mehrmals aufzurufen und die Ergebnisse auf der Clientseite zu sammeln. Wahrscheinlich wäre es effizienter, eine neue Anweisung mit einer benutzerdefinierten Anzahl von? aber jedes Mal.
Chris Mazzola
3

Sie können die setArray-Methode wie in diesem Javadoc erwähnt verwenden :

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();
Panky031
quelle
2
Dies wird nicht von allen Treibern unterstützt. Wenn die Funktion nicht unterstützt wird, erhalten Sie SQLFeatureNotSupportedException
unbenannt
Leider unterstützt mein Treiber es nicht
EdXX
Dies funktioniert nicht für Oracle
Santosh Raviteja
3

Sie können Collections.nCopieseine Sammlung von Platzhaltern generieren und diese verbinden, indem Sie 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
     */
}
Gurwinder Singh
quelle
Scheint die bisher beste Lösung zu sein, wenn Oracle JDBC verwendet wird ...
jansohn
2

Hier ist eine vollständige Lösung in Java, um die vorbereitete Anweisung für Sie zu erstellen:

/*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;

    }

}
Dwjohnston
quelle
2

Mit Spring können Sie java.util.Lists an NamedParameterJdbcTemplate übergeben , wodurch die Generierung von (?,?,?, ...,?) nach Anzahl der Argumente automatisiert wird.

In diesem Blogbeitrag für Oracle wird die Verwendung von oracle.sql.ARRAY erläutert (Connection.createArrayOf funktioniert nicht mit Oracle). Dazu müssen Sie Ihre SQL-Anweisung ändern:

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

Die Oracle-Tabellenfunktion wandelt das übergebene Array in einen tabellenähnlichen Wert um, der in der INAnweisung verwendet werden kann.

Hans-Peter Störr
quelle
1

versuchen Sie es mit der Instr-Funktion?

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

dann

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

Zugegeben, dies ist ein schmutziger Hack, aber es verringert die Möglichkeiten für die SQL-Injektion. Funktioniert sowieso im Orakel.

stjohnroe
quelle
Oh, und ich bin mir bewusst, dass es keine Indizes verwenden wird
stjohnroe
Bei einigen Zeichenfolgen würde dies beispielsweise nicht funktionieren, wenn die Zeichenfolge ein ',' enthält.
David Portabella
1

Sormula unterstützt den SQL IN-Operator, indem Sie ein java.util.Collection-Objekt als Parameter angeben können . Es erstellt eine vorbereitete Aussage mit einem? für jedes der Elemente die Sammlung. Siehe Beispiel 4 (SQL im Beispiel ist ein Kommentar, um zu verdeutlichen, was erstellt, aber nicht von Sormula verwendet wird).

Jeff Miller
quelle
1

anstatt zu verwenden

SELECT my_column FROM my_table where search_column IN (?)

Verwenden Sie die SQL-Anweisung als

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

und

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

oder verwenden Sie eine gespeicherte Prozedur. Dies wäre die beste Lösung, da die SQL-Anweisungen kompiliert und auf dem DataBase-Server gespeichert werden

kapil das
quelle
1

Ich bin auf eine Reihe von Einschränkungen im Zusammenhang mit vorbereiteten Aussagen gestoßen:

  1. Die vorbereiteten Anweisungen werden nur innerhalb derselben Sitzung (Postgres) zwischengespeichert, sodass sie nur mit Verbindungspooling funktionieren
  2. Viele verschiedene vorbereitete Anweisungen, wie von @BalusC vorgeschlagen, können dazu führen, dass der Cache überfüllt wird und zuvor zwischengespeicherte Anweisungen gelöscht werden
  3. Die Abfrage muss optimiert werden und Indizes verwenden. Klingt offensichtlich, aber z. B. kann die von @Boris in einer der Top-Antworten vorgeschlagene ANY (ARRAY ...) - Anweisung keine Indizes verwenden, und die Abfrage ist trotz Caching langsam
  4. Die vorbereitete Anweisung speichert auch den Abfrageplan zwischen und die tatsächlichen Werte aller in der Anweisung angegebenen Parameter sind nicht verfügbar.

Unter den vorgeschlagenen Lösungen würde ich die auswählen, die die Abfrageleistung nicht verringert und die Anzahl der Abfragen verringert. Dies ist die Nummer 4 (Stapel von wenigen Abfragen) über den @ Don-Link oder die Angabe von NULL-Werten für nicht benötigtes '?' Noten wie von @Vladimir Dyuzhev vorgeschlagen

Alexander
quelle
1

Ich habe gerade eine PostgreSQL-spezifische Option dafür ausgearbeitet. Es ist ein bisschen wie ein Hack und hat seine eigenen Vor- und Nachteile und Einschränkungen, aber es scheint zu funktionieren und ist nicht auf eine bestimmte Entwicklungssprache, Plattform oder einen bestimmten PG-Treiber beschränkt.

Der Trick besteht natürlich darin, einen Weg zu finden, eine Sammlung von Werten beliebiger Länge als einen einzelnen Parameter zu übergeben und die Datenbank als mehrere Werte erkennen zu lassen. Die Lösung, an der ich arbeite, besteht darin, aus den Werten in der Auflistung eine begrenzte Zeichenfolge zu erstellen, diese Zeichenfolge als einzelnen Parameter zu übergeben und string_to_array () mit dem erforderlichen Casting für PostgreSQL zu verwenden, um sie ordnungsgemäß zu verwenden.

Wenn Sie also nach "foo", "blah" und "abc" suchen möchten, können Sie sie zu einer einzigen Zeichenfolge zusammenfassen: 'foo, blah, abc'. Hier ist das reine SQL:

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

Sie würden natürlich die explizite Umwandlung in das ändern, was Ihr resultierendes Wertearray sein soll - int, text, uuid usw. Und weil die Funktion einen einzelnen Zeichenfolgenwert annimmt (oder zwei, nehme ich an, wenn Sie das Trennzeichen anpassen möchten Sie können es auch als Parameter in einer vorbereiteten Anweisung übergeben:

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

Dies ist sogar flexibel genug, um Dinge wie LIKE-Vergleiche zu unterstützen:

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

Keine Frage, es ist ein Hack, aber es funktioniert und ermöglicht es Ihnen, vorkompilierte vorbereitete Anweisungen zu verwenden, die * ahem * diskrete Parameter mit den damit verbundenen Sicherheits- und (möglicherweise) Leistungsvorteilen verwenden. Ist es ratsam und tatsächlich performant? Dies hängt natürlich davon ab, ob Sie das Parsen von Zeichenfolgen und möglicherweise das Casting durchführen, bevor Ihre Abfrage überhaupt ausgeführt wird. Wenn Sie damit rechnen, drei, fünf, ein paar Dutzend Werte zu senden, ist das wahrscheinlich in Ordnung. Ein paar tausend? Ja, vielleicht nicht so sehr. YMMV, Einschränkungen und Ausschlüsse gelten, keine ausdrückliche oder stillschweigende Garantie.

Aber es funktioniert.

Joel Fouse
quelle
0

Nur der Vollständigkeit halber: Solange die Menge der Werte nicht zu groß ist, man könnte auch einfach String-Konstrukt eine Anweisung wie

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

Diese können Sie dann übergeben, um prepare () zu erstellen, und dann setXXX () in einer Schleife verwenden, um alle Werte festzulegen. Das sieht gut aus, aber viele "große" kommerzielle Systeme tun dies routinemäßig, bis sie DB-spezifische Grenzen erreichen, wie z. B. 32 KB (glaube ich) für Anweisungen in Oracle.

Natürlich müssen Sie sicherstellen, dass der Satz niemals unangemessen groß wird, oder in diesem Fall Fehler einfangen.

Carl Smotricz
quelle
Ja, du hast Recht. Mein Ziel in diesem Fall war es, das PreparedStatement jedes Mal mit einer unterschiedlichen Anzahl von Elementen wiederzuverwenden.
Chris Mazzola
3
Die Verwendung von "ODER" würde die Absicht verschleiern. Halten Sie sich an "IN", da es leichter zu lesen ist und die Absicht klarer ist. Der einzige Grund für einen Wechsel besteht darin, dass die Abfragepläne unterschiedlich waren.
James Schek
0

Nach Adams Idee. Stellen Sie Ihre vorbereitete Anweisung so ein, dass Sie my_column aus my_table auswählen, wobei search_column in (#) einen String x erstellt und mit einer Zahl von "?,?,?" abhängig von Ihrer Werteliste Ändern Sie dann einfach das # in der Abfrage für Ihren neuen String x und füllen Sie ihn aus


quelle
0

Generieren Sie die Abfragezeichenfolge im PreparedStatement so, dass eine Anzahl von? Mit der Anzahl der Elemente in Ihrer Liste übereinstimmt. Hier ist ein Beispiel:

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;
}
neu242
quelle
5
StringBuilder muss nicht mehr verwendet werden. Der Compiler konvertiert die + -Zeichen ohnehin in StringBuilder.append (), sodass keine Leistungseinbußen auftreten. Probieren Sie es aus :)
neu242
5
@ neu242: Oh ja, der Compiler verwendet StringBuilder. Aber nicht so, wie du denkst. Beim Dekompilieren können generateQsForInSie sehen, dass pro Schleifeniteration zwei neue StringBuilderzugewiesen werden und jeweils toStringaufgerufen werden. Die StringBuilderOptimierung fängt nur Dinge wie ein "x" + i+ "y" + j, geht aber nicht über einen Ausdruck hinaus.
AH
@ neu242 Kannst du nicht verwenden, ps.setObject(1,items)anstatt die Liste zu durchlaufen und dann die zu setzen paramteres?
Neha Choudhary
0

Es gibt verschiedene alternative Ansätze, die wir für die IN-Klausel in PreparedStatement verwenden können.

  1. Verwenden einzelner Abfragen - langsamste Leistung und ressourcenintensiv
  2. Verwenden von StoredProcedure - Am schnellsten, aber datenbankspezifisch
  3. Dynamische Abfrage für PreparedStatement erstellen - Gute Leistung, profitiert jedoch nicht vom Caching, und PreparedStatement wird jedes Mal neu kompiliert.
  4. Verwenden Sie NULL in PreparedStatement-Abfragen - Optimale Leistung, funktioniert hervorragend, wenn Sie die Grenze der IN-Klauselargumente kennen. Wenn es keine Begrenzung gibt, können Sie Abfragen im Stapel ausführen. Beispielcode-Snippet ist;

        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);
        }

Weitere Details zu diesen alternativen Ansätzen finden Sie hier .

Pankaj
quelle
"Erstellen einer dynamischen Abfrage für PreparedStatement - Gute Leistung, profitiert jedoch nicht vom Caching, und PreparedStatement wird jedes Mal neu kompiliert." Durch das Zwischenspeichern und Vermeiden von Neukompilierungen kann eine vorbereitete Anweisung eine gute Leistung erbringen. Daher stimme ich Ihrer Behauptung nicht zu. Dies verhindert jedoch die SQL-Injection, da Sie die verkettete / dynamische Eingabe auf ein Komma beschränken.
Brandon
Ich stimme Ihnen zu, jedoch ist "Gute Leistung" hier für dieses spezielle Szenario. Es ist besser als Ansatz 1, jedoch ist Ansatz 2 am schnellsten.
Pankaj
0

In einigen Situationen kann regulärer Ausdruck hilfreich sein. Hier ist ein Beispiel, das ich bei Oracle überprüft habe und das funktioniert.

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

Es gibt jedoch eine Reihe von Nachteilen:

  1. Jede angewendete Spalte sollte zumindest implizit in varchar / char konvertiert werden.
  2. Seien Sie vorsichtig mit Sonderzeichen.
  3. Dies kann die Leistung beeinträchtigen. In meinem Fall verwendet die IN-Version den Index- und Bereichsscan und die REGEXP-Version den vollständigen Scan.
Vasili
quelle
0

Nachdem ich verschiedene Lösungen in verschiedenen Foren untersucht und keine gute Lösung gefunden habe, bin ich der Meinung, dass der folgende Hack, den ich mir ausgedacht habe, am einfachsten zu befolgen und zu codieren ist:

Beispiel: Angenommen, Sie müssen mehrere Parameter in der 'IN'-Klausel übergeben. Fügen Sie einfach einen Dummy-String in die 'IN'-Klausel ein. Sagen Sie "PARAM". Bezeichnen Sie die Liste der Parameter, die anstelle dieses Dummy-Strings verwendet werden.

    select * from TABLE_A where ATTR IN (PARAM);

Sie können alle Parameter in einer einzigen String-Variablen in Ihrem Java-Code sammeln. Dies kann wie folgt erfolgen:

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

In unserem Fall können Sie alle durch Kommas getrennten Parameter in eine einzige String-Variable 'param1' einfügen.

Nachdem Sie alle Parameter in einem einzigen String zusammengefasst haben, können Sie einfach den Dummy-Text in Ihrer Abfrage, in diesem Fall "PARAM", durch den Parameter String, dh param1, ersetzen. Folgendes müssen Sie tun:

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

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

Sie können Ihre Abfrage jetzt mit der Methode executeQuery () ausführen. Stellen Sie einfach sicher, dass Ihre Abfrage nirgendwo das Wort "PARAM" enthält. Sie können anstelle des Wortes "PARAM" eine Kombination aus Sonderzeichen und Alphabeten verwenden, um sicherzustellen, dass kein solches Wort in die Abfrage aufgenommen wird. Hoffe du hast die Lösung.

Hinweis: Obwohl dies keine vorbereitete Abfrage ist, erledigt sie die Arbeit, die mein Code ausführen soll.

bnsk
quelle
0

Nur der Vollständigkeit halber und weil ich niemanden gesehen habe, der es vorschlägt:

Bevor Sie einen der oben genannten komplizierten Vorschläge implementieren, prüfen Sie, ob die SQL-Injection in Ihrem Szenario tatsächlich ein Problem darstellt.

In vielen Fällen ist der für IN (...) bereitgestellte Wert eine Liste von IDs, die so generiert wurden, dass Sie sicher sein können, dass keine Injektion möglich ist ... (z. B. die Ergebnisse einer vorherigen Auswahl von some_id aus some_table where some_condition.)

In diesem Fall können Sie diesen Wert einfach verketten und die Dienste oder die vorbereitete Anweisung nicht oder für andere Parameter dieser Abfrage verwenden.

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

PreparedStatement bietet keine gute Möglichkeit, mit der SQL IN-Klausel umzugehen. Per http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Sie können keine Dinge ersetzen, die Teil der SQL-Anweisung werden sollen. Dies ist erforderlich, da sich das SQL selbst ändern kann, wenn Der Treiber kann die Anweisung nicht vorkompilieren. Sie hat auch den netten Nebeneffekt, dass SQL-Injection-Angriffe verhindert werden. " Am Ende habe ich folgenden Ansatz gewählt:

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);
pedram bashiri
quelle
0

SetArray ist die beste Lösung, aber für viele ältere Treiber nicht verfügbar. Die folgende Problemumgehung kann in Java8 verwendet werden

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);
}

Diese Lösung ist besser als andere hässliche while-Schleifenlösungen, bei denen die Abfragezeichenfolge durch manuelle Iterationen erstellt wird

Raheel
quelle
0

Das hat bei mir funktioniert (Pseudocode):

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)";
}

Bindung angeben:

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)
}
Nikita Shah
quelle
0

Mein Beispiel für SQLite- und Oracle-Datenbanken.

Die erste For-Schleife dient zum Erstellen von PreparedStatement-Objekten.

Die zweite For-Schleife dient zum Bereitstellen von Werten für PreparedStatement-Parameter.

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;
}
vstavskyi
quelle
-3

Meine Problemumgehung (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 ist das Array, das Ihre Eingaben / Schlüssel / Felder usw. enthält

glatt_smoothie
quelle