PreparedStatement IN clausole alternative?


343

Quali sono le soluzioni alternative migliori per l'utilizzo di una INclausola SQL con istanze di java.sql.PreparedStatement, che non è supportata per più valori a causa di problemi di sicurezza degli attacchi SQL injection: Un ?segnaposto rappresenta un valore, anziché un elenco di valori.

Considera la seguente istruzione SQL:

SELECT my_column FROM my_table where search_column IN (?)

L'utilizzo preparedStatement.setString( 1, "'A', 'B', 'C'" );è essenzialmente un tentativo non funzionante di risolvere i motivi dell'utilizzo ?in primo luogo.

Quali soluzioni alternative sono disponibili?


1
Oscar, penso che la generazione dinamica di (?,?, ....) sia la soluzione più semplice se hai bisogno di una clausola IN, ma l'ho lasciata alle singole chiamate poiché le prestazioni erano sufficienti nel mio caso specifico.
Chris Mazzola,

6
Uno dei vantaggi delle dichiarazioni preparate è che sohuld può essere compilato una volta per efficienza. Rendendo dinamica la clausola in, ciò nega efficacemente l'affermazione preparata.

2
In realtà, questo funziona per MySQL (usando setObject per impostare una matrice di String come valore del parametro). Quale DB stai usando?
Frans,


Ecco una domanda correlata: stackoverflow.com/q/6956025/521799
Lukas Eder

Risposte:


194

Un'analisi delle varie opzioni disponibili e i pro e contro di ciascuna è disponibile qui .

Le opzioni suggerite sono:

  • Preparare SELECT my_column FROM my_table WHERE search_column = ?, eseguirlo per ogni valore e UNION i risultati sul lato client. Richiede solo una dichiarazione preparata. Lento e doloroso.
  • Preparalo SELECT my_column FROM my_table WHERE search_column IN (?,?,?)ed eseguilo. Richiede un'istruzione preparata per dimensione dell'elenco IN. Veloce e ovvio.
  • Preparalo SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...ed eseguilo. [O utilizzare UNION ALLal posto di quei punti e virgola. --ed] Richiede un'istruzione preparata per ciascuna dimensione di elenco IN. Stupidamente lento, molto peggio di WHERE search_column IN (?,?,?)così, quindi non so perché il blogger lo abbia persino suggerito.
  • Utilizzare una procedura memorizzata per costruire il set di risultati.
  • Preparare N diverse query di dimensioni in elenco; ad esempio, con 2, 10 e 50 valori. Per cercare un elenco IN con 6 valori diversi, popolare la query size-10 in modo che appaia SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Qualsiasi server decente ottimizzerà i valori duplicati prima di eseguire la query.

Nessuna di queste opzioni è comunque eccezionale.

Risposte a domande duplicate in questi luoghi con alternative altrettanto sane, ancora nessuna superlativa:

La risposta giusta, se si utilizza JDBC4 e un server che supporta x = ANY(y), è utilizzare PreparedStatement.setArraycome descritto qui:

Tuttavia, non sembra esserci alcun modo per far setArrayfunzionare gli elenchi IN.


A volte le istruzioni SQL vengono caricate in fase di esecuzione (ad es. Da un file delle proprietà) ma richiedono un numero variabile di parametri. In tali casi, definire innanzitutto la query:

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

Successivamente, carica la query. Quindi determinare il numero di parametri prima di eseguirlo. Una volta noto il conteggio dei parametri, eseguire:

sql = any( sql, count );

Per esempio:

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

Per alcuni database in cui il passaggio di un array tramite la specifica JDBC 4 non è supportato, questo metodo può facilitare la trasformazione del lento = ?in IN (?)condizione di clausola più veloce , che può quindi essere espansa chiamando il anymetodo.


123

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

o

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
sembra buono. quale parte di questo codice è specifica di PostreSQL? il "dove search_column = ANY (?)"? o connection.createArrayOf? o qualcos'altro?
David Portabella,

1
Penso che sia più specifico di JDBC4 rispetto a PostgreSQL, a causa della .createArrayOf()parte, ma non sono sicuro che la semantica rigorosa per l'utente Arraysia definita dalle specifiche JDBC.
lvella,

3
Se .createArrayOfnon funziona, puoi creare la tua creazione manuale di array letterali come String arrayLiteral = "{A,\"B \", C,D}" (nota che "B" ha uno spazio mentre C non funziona) e statement.setString(1,arrayLiteral)dove si trova l'istruzione preparata ... IN (SELECT UNNEST(?::VARCHAR[]))o ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: Non credo che ANYSELECT
funzioni

Ottima soluzione! Mi ha davvero salvato la giornata. Per l'array intero ho usato "int" nel primo parametro di createArrayOf () e sembra buono. Quel primo parametro appare specifico del DB, tuttavia basato sulla documentazione.
Emmanuel Touzery,

2
Questa sembra la soluzione più pulita. Se qualcuno è alla ricerca della sintassi specifica di HSQLDB: sono riuscito a farlo funzionare con IN (UNNEST (?))
aureianimus

19

Nessun modo semplice AFAIK. Se l'obiettivo è mantenere un rapporto cache delle istruzioni elevato (ovvero non creare un'istruzione per ogni conteggio dei parametri), è possibile effettuare le seguenti operazioni:

  1. creare un'istruzione con alcuni (es. 10) parametri:

    ... DOVE UN IN (?,?,?,?,?,?,?,?,?,?) ...

  2. Associare tutti i parametri attuall

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

  3. Lega il resto come NULL

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

NULL non corrisponde mai a nulla, quindi viene ottimizzato dal builder di piani SQL.

La logica è facile da automatizzare quando si passa un elenco a una funzione 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 non corrisponde mai a nulla" - NULLNella query corrisponderebbe a un NULLvalore nel database?
Craig McQueen,

5
@CraigMcQueen No, non lo sarebbe. Null non corrisponde nemmeno a null, secondo lo standard ANSI.
Dawood ibn Kareem,

Puoi abbinare NULL usando la parola chiave IS NULL. Un buon modo per rilevare le righe che non esistono nella tabella unita è utilizzare un JOIN SINISTRA insieme a IS NULL. 'SELECT a.URL, b.URL FROM TABLE_A a LEFT JOIN TABLE_B b ON a_A.URL = b_B.URL DOVE b.URL È NULL' Questo mostrerà tutte le righe nella tabella A che non hanno corrispondenza nella tabella B.
Jens Tandstad,

3
Stai attento con questo però. NOT INe INnon gestiscono i null allo stesso modo. Esegui questo e vedi cosa succede: select 'Matched' as did_it_match where 1 not in (5, null); quindi rimuovi il nulle osserva la magia.
Brandon,

Oppure puoi impostare tutti i parametri extra sul valore di qualsiasi parametro precedente. Qualsiasi motore DB decente li filtrerà. Così a IN (1,2,3,3,3,3,3)è lo stesso a IN (1,2,3). Funziona anche con NOT INunlike a NOT IN (1,2,3,null,null,null,null)(che non restituisce sempre righe come any_value != NULLè sempre falso).
Ruslan Stelmachenko,

11

Una soluzione spiacevole, ma sicuramente fattibile, è utilizzare una query nidificata. Crea una tabella temporanea MYVALUES con una colonna al suo interno. Inserisci il tuo elenco di valori nella tabella MYVALUES. Quindi eseguire

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

Brutta, ma una valida alternativa se la tua lista di valori è molto grande.

Questa tecnica ha l'ulteriore vantaggio di piani di query potenzialmente migliori dell'ottimizzatore (controllare una pagina per più valori, le tabelle possono scansionare una sola volta anziché una volta per valore, ecc.), Se il database non memorizza nella cache le istruzioni preparate. I tuoi "INSERTI" dovrebbero essere eseguiti in batch e potrebbe essere necessario modificare la tabella MYVALUES per avere un blocco minimo o altre protezioni dall'alto.


Quali vantaggi avrebbe rispetto alla query di my_table un valore alla volta?
Paul Tomblin,

3
Query Optimizer può ridurre il carico di I / O recuperando tutte le possibili corrispondenze da una pagina caricata. I piani di scansione di tabelle o indici possono essere eseguiti una volta anziché una volta per valore. Il sovraccarico per l'inserimento di valori può essere ridotto con le operazioni batch e può essere inferiore a diverse query.
James Schek,

1
sembra buono, ma potrebbero esserci problemi con la concorrenza. la specifica jdbc contiene un modo per creare una tabella anonima temporale in memoria? o qualcosa del genere, se possibile non specifico di jdbc-vendor?
David Portabella,

9

I limiti dell'operatore in () sono la radice di tutti i mali.

Funziona per casi banali e puoi estenderlo con "generazione automatica dell'istruzione preparata", tuttavia ha sempre i suoi limiti.

  • se stai creando un'istruzione con un numero variabile di parametri, ciò comporterà un overhead di analisi sql ad ogni chiamata
  • su molte piattaforme, il numero di parametri dell'operatore in () è limitato
  • su tutte le piattaforme, la dimensione totale del testo SQL è limitata, rendendo impossibile l'invio di 2000 segnaposto per i parametri in
  • l'invio di variabili bind di 1000-10k non è possibile, poiché il driver JDBC sta avendo i suoi limiti

L'approccio in () può essere abbastanza buono per alcuni casi, ma non a prova di razzo :)

La soluzione a prova di razzo è quella di passare il numero arbitrario di parametri in una chiamata separata (passando ad esempio un clob di parametri) e quindi avere una vista (o qualsiasi altro modo) per rappresentarli in SQL e utilizzarli nel proprio dove criteri.

Una variante di forza bruta è qui http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Tuttavia, se è possibile utilizzare PL / SQL, questo pasticcio può diventare piuttosto ordinato.

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;

Quindi puoi passare un numero arbitrario di ID cliente separati da virgola nel parametro e:

  • non otterrà alcun ritardo di analisi, poiché l'SQL per select è stabile
  • nessuna complessità delle funzioni pipeline - è solo una query
  • l'SQL sta usando un semplice join, anziché un operatore IN, che è abbastanza veloce
  • dopotutto, è una buona regola empirica di non colpire il database con una semplice selezione o DML, poiché è Oracle, che offre anni luce di più di MySQL o motori di database semplici simili. PL / SQL consente di nascondere il modello di archiviazione dal modello di dominio dell'applicazione in modo efficace.

Il trucco qui è:

  • abbiamo bisogno di una chiamata che accetti la stringa lunga e la memorizzi da qualche parte dove la sessione db può accedervi (ad es. semplice variabile del pacchetto o dbms_session.set_context)
  • allora abbiamo bisogno di una vista che può analizzare questo in righe
  • e poi hai una vista che contiene gli ID che stai interrogando, quindi tutto ciò di cui hai bisogno è un semplice join alla tabella interrogata.

La vista è simile a:

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

dove aux_in_list.getpayload si riferisce alla stringa di input originale.


Un possibile approccio potrebbe essere il passaggio di matrici pl / sql (supportate solo da Oracle), tuttavia non è possibile utilizzarle in SQL puro, pertanto è sempre necessario un passaggio di conversione. La conversione non può essere eseguita in SQL, quindi, dopo tutto, passare una clob con tutti i parametri nella stringa e convertirla in una vista è la soluzione più efficiente.


6

Ecco come l'ho risolto nella mia applicazione. Idealmente, dovresti usare StringBuilder invece di usare + per le stringhe.

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

L'uso di una variabile come x sopra invece di numeri concreti aiuta molto se si decide di modificare la query in un secondo momento.


5

Non l'ho mai provato, ma .setArray () farebbe quello che stai cercando?

Aggiornamento : Evidentemente no. setArray sembra funzionare solo con un java.sql.Array che proviene da una colonna ARRAY recuperata da una query precedente o da una sottoquery con una colonna ARRAY.


4
Non funziona con tutti i database, ma è l'approccio "corretto".
Skaffman,

Intendi tutti i driver. Alcuni driver hanno equivalenti proprietari dello standard di questo anno (il secolo scorso?). Un altro modo è inserire una serie di valori in una tabella temporanea, ma non tutti i database supportano che ...
Tom Hawtin - affronta il

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Secondo Sun, il contenuto dell'array [in genere] rimane sul lato server e viene estratto quando necessario. PreparedStatement.setArray () può rispedire un array da un ResultSet precedente, non creare un nuovo array sul lato client.
Chris Mazzola,

5

La mia soluzione alternativa è:

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

Ora puoi usare una variabile per ottenere alcuni valori in una tabella:

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

Quindi, la dichiarazione preparata potrebbe essere:

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

Saluti,

Javier Ibanez


Questo è PL / SQL, sì. Non funzionerà in altri database. Si noti che questa implementazione ha una limitazione dei parametri di input - la lunghezza totale è limitata a 32k caratteri - nonché una limitazione delle prestazioni poiché la chiamata alla funzione pipeline fa un cambio di contesto tra i motori PL / SQL e SQL di Oracle.
Gee Bee,

3

Suppongo che tu possa (usando la manipolazione di stringa di base) generare la stringa di query in PreparedStatementun numero di? corrispondenti al numero di elementi nella tua lista.

Ovviamente se lo stai facendo, sei solo a un passo dalla generazione di un gigantesco incatenato ORnella tua query, ma senza avere il giusto numero di ?nella stringa di query, non vedo come altro puoi aggirare questo.


Non è davvero una soluzione per me poiché desidero inviare un numero diverso di? ogni volta che chiamo il ps. Ma non pensare di non averlo considerato. : P
Chris Mazzola,

4
Un altro trucco: puoi usare un gran numero di segnaposto di parametri - fino all'elenco di valori più lungo che avrai - e se il tuo elenco di valori è più corto, puoi ripetere i valori: ... DOVE campo di ricerca IN (? ,?,?,?,?,?,?,?) e quindi fornire i valori: A, B, C, D, A, B, C, D
Bill Karwin,

1
Ma nel complesso preferisco la soluzione di Adam: generare SQL in modo dinamico e concatenare? segnaposto per abbinare il numero di valori che devi passare.
Bill Karwin,

Bill, quella soluzione è praticabile se non voglio riutilizzare il PreparedStatement. Un'altra soluzione è quella di effettuare più volte la chiamata al singolo parametro e accumulare i risultati sul lato client. Probabilmente sarebbe più efficiente creare / eseguire una nuova Dichiarazione con un numero personalizzato di? ogni volta però.
Chris Mazzola,

3

È possibile utilizzare il metodo setArray come indicato in questo javadoc :

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
questo non è supportato da tutti i driver, se la funzione non è supportata otterrai SQLFeatureNotSupportedException
senza nome

Purtroppo il mio autista non lo supporta
EdXX

3

Puoi usare Collections.nCopiesper generare una raccolta di segnaposto e unirti a loro usando 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
     */
}

Sembra essere la migliore soluzione finora quando si utilizza Oracle JDBC ...
jansohn

2

Ecco una soluzione completa in Java per creare l'istruzione preparata per te:

/*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 consente il passaggio di java.util.Lists a NamedParameterJdbcTemplate , che automatizza la generazione di (?,?,?, ...,?), In base al numero di argomenti.

Per Oracle, questo post sul blog discute l'uso di oracle.sql.ARRAY (Connection.createArrayOf non funziona con Oracle). Per questo devi modificare la tua istruzione SQL:

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

La funzione tabella oracle trasforma l'array passato in una tabella come valore utilizzabile INnell'istruzione.


1

provare a usare la funzione instr?

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

poi

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

È vero che questo è un po 'un trucco sporco, ma riduce le opportunità di iniezione sql. Funziona comunque in oracolo.


Oh, e sono consapevole che non utilizzerà gli indici
stjohnroe,

non funzionerebbe per alcune stringhe, ad esempio, se la stringa contiene un ','.
David Portabella,

1

Sormula supporta l'operatore SQL IN consentendo di fornire un oggetto java.util.Collection come parametro. Crea un'istruzione preparata con un? per ciascuno degli elementi la collezione. Vedi Esempio 4 (SQL in esempio è un commento per chiarire cosa viene creato ma non utilizzato da Sormula).


1

invece di usare

SELECT my_column FROM my_table where search_column IN (?)

utilizzare la dichiarazione Sql come

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

e

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

o utilizzare una procedura memorizzata questa sarebbe la soluzione migliore, poiché le istruzioni sql verranno compilate e archiviate nel server DataBase


1

Mi sono imbattuto in una serie di limitazioni relative alla dichiarazione preparata:

  1. Le istruzioni preparate vengono memorizzate nella cache solo all'interno della stessa sessione (Postgres), quindi funzionerà davvero solo con il pool di connessioni
  2. Molte diverse istruzioni preparate come proposto da @BalusC possono causare il riempimento eccessivo della cache e l'eliminazione delle istruzioni precedentemente memorizzate nella cache
  3. La query deve essere ottimizzata e utilizzare gli indici. Sembra ovvio, ad esempio l'istruzione ANY (ARRAY ...) proposta da @Boris in una delle risposte migliori non può utilizzare gli indici e la query sarà lenta nonostante la memorizzazione nella cache
  4. L'istruzione preparata memorizza nella cache anche il piano di query e i valori effettivi di tutti i parametri specificati nell'istruzione non sono disponibili.

Tra le soluzioni proposte sceglierei quella che non riduce le prestazioni della query e fa il minor numero di query. Questo sarà il numero 4 (raggruppamento di poche query) dal collegamento @Don o specificando i valori NULL per "?" Non necessari segni come proposto da @Vladimir Dyuzhev


1

Ho appena elaborato un'opzione specifica PostgreSQL per questo. È un po 'un hack e viene fornito con i suoi pro, contro e limiti, ma sembra funzionare e non si limita a un linguaggio di sviluppo specifico, una piattaforma o un driver PG.

Il trucco ovviamente è trovare un modo per passare una raccolta di valori di lunghezza arbitraria come singolo parametro e fare in modo che il db lo riconosca come valori multipli. La soluzione che sto lavorando è quella di costruire una stringa delimitata dai valori nella raccolta, passare quella stringa come singolo parametro e usare string_to_array () con il cast necessario per PostgreSQL per usarlo correttamente.

Quindi, se si desidera cercare "foo", "blah" e "abc", è possibile concatenarli insieme in un'unica stringa come: "foo, blah, abc". Ecco il semplice SQL:

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

Ovviamente cambieresti il ​​cast esplicito in qualunque cosa tu volessi che fosse la tua matrice di valori risultante - int, text, uuid, ecc. E poiché la funzione sta prendendo un singolo valore di stringa (o due suppongo, se vuoi personalizzare il delimitatore anche), puoi passarlo come parametro in un'istruzione preparata:

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

Questo è persino abbastanza flessibile da supportare cose come i confronti LIKE:

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

Ancora una volta, non c'è dubbio che sia un hack, ma funziona e consente di utilizzare ancora istruzioni preparate precompilate che accettano parametri * ahem * discreti, con i relativi vantaggi di sicurezza e (forse) prestazionali. È consigliabile e realmente performante? Naturalmente, dipende dal fatto che hai l'analisi delle stringhe e che il casting potrebbe essere in corso prima ancora che la tua query venga eseguita. Se ti aspetti di inviare tre, cinque, alcune decine di valori, sicuramente va bene. Qualche migliaio? Sì, forse non così tanto. Si applicano YMMV, limitazioni ed esclusioni, nessuna garanzia espressa o implicita.

Ma funziona


0

Solo per completezza: Fino a quando l'insieme dei valori non è troppo grande, si potrebbe anche semplicemente stringa-costruire una dichiarazione come

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

che è possibile passare a prepar (), quindi utilizzare setXXX () in un ciclo per impostare tutti i valori. Sembra sfortunato, ma molti "grandi" sistemi commerciali fanno regolarmente questo genere di cose fino a quando non raggiungono limiti specifici del DB, come 32 KB (penso che sia) per le dichiarazioni in Oracle.

Ovviamente devi assicurarti che il set non sarà mai irragionevolmente grande, o fare un trapping di errore nel caso lo fosse.


Sì hai ragione. Il mio obiettivo in questo caso era di riutilizzare ogni volta il PreparedStatement con un numero diverso di elementi.
Chris Mazzola,

3
L'uso di "OR" offuscherebbe l'intento. Attenersi a "IN" perché è più facile da leggere e l'intento è più chiaro. L'unico motivo per passare è se i piani di query erano diversi.
James Schek,

0

Seguendo l'idea di Adam. Rendi la tua istruzione preparata una sorta di selezione di my_column da my_table dove search_column in (#) Crea una stringa x e riempila con un numero di "?,?,?" a seconda del tuo elenco di valori Quindi modifica semplicemente # nella query per la tua nuova stringa x un popolato


0

Generare la stringa di query in PreparedStatement in modo che un numero di? 'Corrisponda al numero di elementi nell'elenco. Ecco un esempio:

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
Non è più necessario utilizzare StringBuilder. Il compilatore converte comunque i segni + in StringBuilder.append (), quindi non vi è alcun impatto sulle prestazioni.
Mettiti alla

5
@ neu242: Oh sì, il compilatore usa StringBuilder. Ma non nel modo in cui pensi. Decompilando generateQsForInpuoi vedere che per ogni iterazione di loop sono allocati due nuovi StringBuildere toStringviene chiamato su ciascuno. L' StringBuilderottimizzazione rileva solo cose come "x" + i+ "y" + jma non si estende oltre un'espressione.
AH,

@ neu242 Non puoi usare ps.setObject(1,items)invece di scorrere sull'elenco e quindi impostare paramteres?
Neha Choudhary,

0

Esistono diversi approcci alternativi che è possibile utilizzare per la clausola IN in PreparedStatement.

  1. Utilizzo di query singole: prestazioni più lente e ad alta intensità di risorse
  2. Uso di StoredProcedure: il più veloce ma specifico per il database
  3. Creazione di query dinamica per PreparedStatement: buone prestazioni ma non beneficiano della memorizzazione nella cache e PreparedStatement viene ricompilato ogni volta.
  4. Usa NULL nelle query PreparedStatement: prestazioni ottimali, funziona perfettamente quando conosci il limite degli argomenti della clausola IN. Se non vi sono limiti, è possibile eseguire query in batch. Lo snippet di codice di esempio è;

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

Puoi controllare maggiori dettagli su questi approcci alternativi qui .


"Creazione di query dinamiche per PreparedStatement: buone prestazioni ma non beneficiano della memorizzazione nella cache e PreparedStatement viene ricompilato ogni volta." la memorizzazione nella cache ed evitare ricompilazioni è ciò che fa funzionare bene un'istruzione preparata. Pertanto, non sono d'accordo con la tua richiesta. Ciò, tuttavia, impedirà l'iniezione di SQL poiché stai limitando l'input concatenato / dinamico a una virgola.
Brandon,

Sono d'accordo con te, tuttavia "Good Performance" qui è per questo scenario specifico. È più performante dell'approccio 1, tuttavia l'approccio 2 è più veloce.
Pankaj,

0

Per alcune situazioni, regexp potrebbe essere d'aiuto. Ecco un esempio che ho verificato su Oracle e funziona.

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

Ma c'è un certo numero di inconvenienti:

  1. Qualsiasi colonna applicata deve essere convertita in varchar / char, almeno implicitamente.
  2. Devi stare attento con i caratteri speciali.
  3. Può rallentare le prestazioni: nel mio caso la versione IN utilizza la scansione dell'indice e dell'intervallo e la versione REGEXP esegue la scansione completa.

0

Dopo aver esaminato varie soluzioni in diversi forum e non aver trovato una buona soluzione, sento che l'hacking di seguito che mi è venuto in mente, è il più facile da seguire e codificare:

Esempio: supponiamo di avere più parametri da passare nella clausola 'IN'. Basta inserire una stringa fittizia nella clausola 'IN', ad esempio "PARAM" indica l'elenco dei parametri che verranno sostituiti da questa stringa fittizia.

    select * from TABLE_A where ATTR IN (PARAM);

Puoi raccogliere tutti i parametri in una singola variabile String nel tuo codice Java. Questo può essere fatto come segue:

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

Nel nostro caso puoi aggiungere tutti i tuoi parametri separati da virgole in una singola variabile String, 'param1'.

Dopo aver raccolto tutti i parametri in una singola stringa, puoi semplicemente sostituire il testo fittizio nella tua query, ad esempio "PARAM" in questo caso, con il parametro String, ovvero param1. Ecco cosa devi fare:

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

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

È ora possibile eseguire la query utilizzando il metodo executeQuery (). Assicurati solo di non avere la parola "PARAM" nella tua query ovunque. È possibile utilizzare una combinazione di caratteri speciali e alfabeti al posto della parola "PARAM" per assicurarsi che non vi sia alcuna possibilità che tale parola venga inserita nella query. Spero che tu abbia la soluzione.

Nota: sebbene questa non sia una query preparata, fa il lavoro che volevo che il mio codice facesse.


0

Solo per completezza e perché non ho visto nessun altro suggerirlo:

Prima di implementare uno dei suggerimenti complicati sopra, considera se l'iniezione di SQL è davvero un problema nel tuo scenario.

In molti casi il valore fornito a IN (...) è un elenco di ID che sono stati generati in modo tale da essere certi che non sia possibile alcuna iniezione ... (ad esempio, i risultati di una precedente selezione some_id da some_table dove some_condition.)

In tal caso, è possibile concatenare questo valore e non utilizzare i servizi o l'istruzione preparata per esso o utilizzarli per altri parametri di questa query.

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

0

PreparedStatement non fornisce alcun buon modo per gestire la clausola SQL IN. Per http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Non è possibile sostituire elementi che dovrebbero diventare parte dell'istruzione SQL. Ciò è necessario perché se lo stesso SQL può cambiare, il il driver non può precompilare l'istruzione. Ha anche il piacevole effetto collaterale di prevenire attacchi di iniezione SQL. " Ho finito con il seguente approccio:

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 è la soluzione migliore ma non è disponibile per molti driver meno recenti. La seguente soluzione alternativa può essere utilizzata in 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);
}

Questa soluzione è migliore di altre brutte soluzioni a ciclo continuo in cui la stringa di query è costruita da iterazioni manuali


0

Questo ha funzionato per me (psuedocode):

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

specificare il legame:

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

Il mio esempio per i database SQLite e Oracle.

Il primo ciclo For è per la creazione di un oggetto PreparedStatement.

Il secondo ciclo For è per la fornitura di valori per i parametri 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

La mia soluzione alternativa (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 è l'array che contiene i tuoi input / chiavi / campi ecc

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.