Alternativas da cláusula PreparedStatement IN?


343

Quais são as melhores soluções para o uso de uma INcláusula SQL com instâncias de java.sql.PreparedStatement, que não são suportadas por vários valores devido a problemas de segurança de ataques de injeção SQL: Um ?espaço reservado representa um valor, em vez de uma lista de valores.

Considere a seguinte instrução SQL:

SELECT my_column FROM my_table where search_column IN (?)

O uso preparedStatement.setString( 1, "'A', 'B', 'C'" );é essencialmente uma tentativa não-útil de uma solução alternativa dos motivos do uso ?em primeiro lugar.

Quais soluções alternativas estão disponíveis?


11
Oscar, acho que a geração dinâmica de (?,?, ....) é a solução mais simples, se você precisar de uma cláusula IN, mas deixei para chamadas individuais, pois o desempenho era suficiente no meu caso específico.
31720 Chris Mazzola

6
Uma das vantagens das declarações preparadas é que o sohuld pode ser compilado uma vez para obter eficiência. Ao dinamizar a cláusula in, isso efetivamente nega a declaração preparada.

2
Na verdade, isso funciona para o MySQL (usando setObject para definir uma matriz de String como o valor do parâmetro). Qual banco de dados você está usando?
Frans


Aqui está uma questão relacionada: stackoverflow.com/q/6956025/521799
Lukas Eder

Respostas:


194

Uma análise das várias opções disponíveis e os prós e contras de cada uma estão disponíveis aqui .

As opções sugeridas são:

  • Prepare SELECT my_column FROM my_table WHERE search_column = ?, execute-o para cada valor e UNIONE os resultados do lado do cliente. Requer apenas uma declaração preparada. Lento e doloroso.
  • Prepare SELECT my_column FROM my_table WHERE search_column IN (?,?,?)e execute. Requer uma declaração preparada por tamanho da lista. Rápido e óbvio.
  • Prepare SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...e execute. [Ou use UNION ALLno lugar desses pontos e vírgulas. --ed] Requer uma declaração preparada por tamanho da lista. Estupidamente lento, estritamente pior do que WHERE search_column IN (?,?,?), então não sei por que o blogueiro sugeriu isso.
  • Use um procedimento armazenado para construir o conjunto de resultados.
  • Prepare N consultas diferentes de tamanho da lista; digamos, com 2, 10 e 50 valores. Para procurar uma lista IN com 6 valores diferentes, preencha a consulta tamanho 10 para que ela se pareça SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Qualquer servidor decente otimizará os valores duplicados antes de executar a consulta.

Nenhuma dessas opções é super ótima, no entanto.

Perguntas duplicadas foram respondidas nesses locais com alternativas igualmente saudáveis, mas nenhuma delas é ótima:

A resposta certa, se você estiver usando o JDBC4 e um servidor compatível x = ANY(y), será usado PreparedStatement.setArrayconforme descrito aqui:

Parece não haver nenhuma maneira de setArraytrabalhar com as listas IN.


Às vezes, as instruções SQL são carregadas em tempo de execução (por exemplo, de um arquivo de propriedades), mas requerem um número variável de parâmetros. Nesses casos, primeiro defina a consulta:

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

Em seguida, carregue a consulta. Em seguida, determine o número de parâmetros antes de executá-lo. Depois que a contagem de parâmetros for conhecida, execute:

sql = any( sql, count );

Por exemplo:

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

Para certos bancos de dados nos quais a passagem de uma matriz pela especificação JDBC 4 não é suportada, esse método pode facilitar a transformação da condição de cláusula lenta = ?em mais rápida IN (?), que pode ser expandida chamando o anymétodo.


123

Solução para 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();
}

ou

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

11
parece bom. qual parte desse código é específica para o PostreSQL? o "where search_column = QUALQUER (?)"? ou o connection.createArrayOf? ou alguma outra coisa?
precisa

11
Eu acho que é mais específico do JDBC4 do que do PostgreSQL, por causa da .createArrayOf()parte, mas não tenho certeza de que a semântica estrita para os usuários Arrayseja definida pela especificação do JDBC.
Lvella

3
Se .createArrayOfnão funcionar, você poderá criar sua própria criação manual de literal de matriz String arrayLiteral = "{A,\"B \", C,D}" (observe que "B" possui um espaço enquanto C não funciona) e, statement.setString(1,arrayLiteral)em seguida, onde a instrução preparada é ... IN (SELECT UNNEST(?::VARCHAR[]))ou ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: Eu não acho que ANYfunciona com um SELECT.)
ADTC

Ótima solução! Realmente salvou o dia para mim. Para array inteiro, usei "int" no primeiro parâmetro de createArrayOf () e está com boa aparência. Esse primeiro parâmetro parece específico do banco de dados, com base na documentação.
Emmanuel Touzery

2
Essa parece a solução mais limpa. Se alguém está procurando a sintaxe específica HSQLDB: eu consegui chegar a este trabalho com a IN (UNNEST ()?)
aureianimus

19

De maneira simples, AFAIK. Se o objetivo é manter a taxa de cache de instruções alta (ou seja, não criar uma instrução para cada contagem de parâmetros), você pode fazer o seguinte:

  1. crie uma declaração com alguns (por exemplo, 10) parâmetros:

    ... ONDE ESTÁ (?,?,?,?,?,?,?,?,?, ...) ...

  2. Vincular todos os parâmetros atuais

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

  3. Vincule o restante como NULL

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

NULL nunca corresponde a nada; portanto, ele é otimizado pelo construtor de planos SQL.

A lógica é fácil de automatizar quando você passa uma Lista para uma função 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 nunca corresponde a nada" - NULLna consulta corresponderia a um NULLvalor no banco de dados?
Craig McQueen

5
@CraigMcQueen Não, não seria. Nulo nem corresponde a nulo, de acordo com o padrão ANSI.
Dawood ibn Kareem

Você pode corresponder a NULL usando a palavra-chave IS NULL. Uma boa maneira de detectar linhas que não existem na tabela unida é usar LEFT JOIN junto com IS NULL. 'SELECT a.URL, b.URL FROM TABLE_A a LEFT JOIN TABLE_B b ON a_A.URL = b_B.URL ONDE b.URL É NULL' Isso mostrará todas as linhas da tabela A que não têm correspondência na tabela B.
Jens Tandstad

3
Tenha cuidado com isso embora. NOT INe INnão manipula nulos da mesma maneira. Execute isso e veja o que acontece: select 'Matched' as did_it_match where 1 not in (5, null); remova o nulle observe a mágica.
Brandon

Ou você pode definir todos os parâmetros extras para o valor de qualquer parâmetro anterior. Qualquer mecanismo de banco de dados decente os filtrará. Então a IN (1,2,3,3,3,3,3)é o mesmo que a IN (1,2,3). Também funciona com NOT INdiferentemente a NOT IN (1,2,3,null,null,null,null)(que sempre retorna sem linhas, como any_value != NULLsempre é falso).
Ruslan Stelmachenko

11

Uma solução desagradável, mas certamente viável é usar uma consulta aninhada. Crie uma tabela temporária MYVALUES com uma coluna nela. Insira sua lista de valores na tabela MYVALUES. Então execute

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

Feia, mas uma alternativa viável se sua lista de valores for muito grande.

Essa técnica tem a vantagem adicional de otimizar planos de consulta potencialmente melhores do otimizador (verifique uma página em busca de vários valores, as tabelas podem apenas uma vez, uma vez por valor, etc.) pode economizar sobrecarga se o banco de dados não armazenar em cache instruções preparadas. Seu "INSERTS" precisaria ser feito em lote e a tabela MYVALUES poderá precisar ser ajustada para ter um bloqueio mínimo ou outras proteções de sobrecarga alta.


Que vantagens isso teria sobre a consulta de my_table, um valor de cada vez?
Paul Tomblin 9/10/08

3
O otimizador de consulta pode reduzir a carga de E / S, recuperando todas as correspondências possíveis de uma página carregada. Tabelas ou varreduras de índice podem ser executadas uma vez em vez de uma vez por valor. As despesas gerais para inserir valores podem ser reduzidas com operações em lote e podem ser menores que várias consultas.
James Schek 9/10/08

11
parece bom, mas pode haver problemas com a simultaneidade. a especificação jdbc contém uma maneira de criar uma tabela anônima temporal na memória? ou algo assim, se possível, não específico do jdbc-vendor?
David Portabella

9

Limitações do operador in () são a raiz de todo mal.

Ele funciona em casos triviais, e você pode estendê-lo com "geração automática da instrução preparada", no entanto, está sempre tendo seus limites.

  • se você estiver criando uma instrução com número variável de parâmetros, isso fará uma análise de sql em cada chamada
  • em muitas plataformas, o número de parâmetros do operador in () é limitado
  • em todas as plataformas, o tamanho total do texto SQL é limitado, impossibilitando o envio de 2000 espaços reservados para os parâmetros
  • não é possível enviar variáveis ​​de ligação de 1000 a 10k, pois o driver JDBC está tendo suas limitações

A abordagem in () pode ser boa o suficiente para alguns casos, mas não é à prova de foguetes :)

A solução à prova de foguetes é passar o número arbitrário de parâmetros em uma chamada separada (passando um clob de parâmetros, por exemplo) e, em seguida, ter uma visualização (ou qualquer outra maneira) para representá-los no SQL e usá-los no seu where critério.

Uma variante de força bruta está aqui http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

No entanto, se você pode usar o PL / SQL, essa bagunça pode se tornar bem organizada.

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;

Em seguida, você pode passar um número arbitrário de IDs de clientes separados por vírgula no parâmetro e:

  • não receberá atraso de análise, pois o SQL for select é estável
  • nenhuma complexidade de funções em pipeline - é apenas uma consulta
  • o SQL está usando uma junção simples, em vez de um operador IN, que é bastante rápido
  • afinal de contas, é uma boa regra geral não atingir o banco de dados com nenhum DML ou select select, já que é o Oracle, que oferece anos de vida superior ao MySQL ou mecanismos de banco de dados simples semelhantes. O PL / SQL permite ocultar o modelo de armazenamento do modelo de domínio do aplicativo de maneira eficaz.

O truque aqui é:

  • precisamos de uma chamada que aceite a cadeia longa e armazene em algum lugar onde a sessão do db possa acessá-la (por exemplo, variável de pacote simples ou dbms_session.set_context)
  • então precisamos de uma visão que possa analisar isso em linhas
  • e, em seguida, você tem uma visualização que contém os IDs que você está consultando; portanto, tudo que você precisa é de uma associação simples à tabela consultada.

A vista é semelhante 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

em que aux_in_list.getpayload se refere à sequência de entrada original.


Uma abordagem possível seria passar matrizes pl / sql (suportadas apenas pelo Oracle); no entanto, você não pode usá-las em SQL puro; portanto, sempre é necessária uma etapa de conversão. A conversão não pode ser feita no SQL; portanto, passar um clob com todos os parâmetros na string e convertê-lo em uma visualização é a solução mais eficiente.


6

Aqui está como eu o resolvi em meu próprio aplicativo. Idealmente, você deve usar um StringBuilder em vez de usar + para Strings.

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

Usar uma variável como x acima em vez de números concretos ajuda muito se você decidir alterar a consulta posteriormente.


tentou o mesmo, não está funcionando para Oracle
Santosh Raviteja

5

Eu nunca tentei, mas faria .setArray () fazer o que você está procurando?

Atualização : Evidentemente não. O setArray parece funcionar apenas com um java.sql.Array que vem de uma coluna ARRAY que você recuperou de uma consulta anterior ou de uma subconsulta com uma coluna ARRAY.


4
Não funciona com todos os bancos de dados, mas é a abordagem "correta".
22468 skaffman

Você quer dizer todos os motoristas. Alguns drivers possuem equivalentes proprietários do padrão deste ano (século passado?). Outra maneira é bung um lote de valores em uma tabela temporária, mas nem todos os bancos de dados suportam que ...
Tom Hawtin - tackline

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Segundo a Sun, o conteúdo da matriz [normalmente] permanece no lado do servidor e é extraído conforme necessário. PreparedStatement.setArray () pode enviar de volta uma matriz de um ResultSet anterior, não criar uma nova matriz no lado do cliente.
9118 Chris Mazzola

5

Minha solução 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;
/

Agora você pode usar uma variável para obter alguns valores em uma tabela:

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

Portanto, a declaração preparada pode ser:

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

Saudações,

Javier Ibanez


Isso é PL / SQL, sim. Não funcionará em outros bancos de dados. Observe que esta implementação possui uma limitação de parâmetros de entrada - o comprimento total é limitado a 32k caracteres -, bem como uma limitação de desempenho, pois a chamada para a função em pipeline faz uma alternância de contexto entre os mecanismos PL / SQL e SQL do Oracle.
Gee Bee

3

Suponho que você possa (usando a manipulação básica de strings) gerar a string de consulta PreparedStatementpara ter um número ?correspondente ao número de itens em sua lista.

É claro que se você estiver fazendo isso, estará a um passo de gerar um gigante encadeado ORem sua consulta, mas sem ter o número certo ?na sequência de consultas, não vejo como mais você pode contornar isso.


Não é realmente uma solução para mim, pois quero enviar um número diferente de? cada vez que ligo para o ps. Mas não pense que eu não tinha considerado. : P
Chris Mazzola

4
Outro truque: você pode usar um grande número de espaços reservados para parâmetros - tantos quanto a lista de valores mais longa que você tiver - e se sua lista de valores for menor, você poderá repetir os valores: ... WHERE campo de pesquisa IN (? ???????,,,,,,,) e, em seguida, fornecer valores: a, B, C, D, a, B, C, D
Bill Karwin

11
Mas, em geral, sou a favor da solução de Adam: gerar o SQL dinamicamente e concatenar? espaços reservados para corresponder ao número de valores que você precisa passar.
Bill Karwin

Bill, essa solução é viável se eu não quiser reutilizar o PreparedStatement. Outra solução é fazer o único parâmetro chamar várias vezes e acumular os resultados no lado do cliente. Provavelmente, seria mais eficiente criar / executar uma nova instrução com número personalizado de? cada vez que.
9118 Chris Mazzola

3

Você pode usar o método setArray conforme mencionado neste 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
isso não é suportado por todos os drivers; se o recurso não for suportado, você obterá SQLFeatureNotSupportedException
sem nome

Infelizmente o meu motorista não apoiá-lo
EdXX

Isso não funciona para Oracle
Santosh Raviteja

3

Você pode usar Collections.nCopiespara gerar uma coleção de espaços reservados e juntá-los 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
     */
}

Parece ser a melhor solução até agora ao usar o Oracle JDBC ...
jansohn 06/03

2

Aqui está uma solução completa em Java para criar a declaração preparada para você:

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

O Spring permite passar java.util.Lists para NamedParameterJdbcTemplate , que automatiza a geração de (?,?,?, ...,?), Conforme apropriado para o número de argumentos.

Para Oracle, esta postagem no blog discute o uso de oracle.sql.ARRAY (Connection.createArrayOf não funciona com Oracle). Para isso, você deve modificar sua instrução SQL:

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

A função da tabela oracle transforma a matriz passada em uma tabela como valor utilizável na INinstrução


1

tente usar a função instr?

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

então

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

É certo que isso é um truque sujo, mas reduz as oportunidades de injeção de sql. Funciona no oracle de qualquer maneira.


Oh, e estou ciente de que não irá utilizar índices
stjohnroe

não funcionaria para algumas strings, por exemplo, se a string contiver um ','.
David Portabella

1

O Sormula suporta o operador SQL IN, permitindo que você forneça um objeto java.util.Collection como parâmetro. Ele cria uma declaração preparada com um? para cada um dos elementos da coleção. Veja o Exemplo 4 (SQL no exemplo é um comentário para esclarecer o que é criado, mas não é usado pelo Sormula).


1

ao invés de usar

SELECT my_column FROM my_table where search_column IN (?)

use a instrução SQL como

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

e

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

ou use um procedimento armazenado, essa seria a melhor solução, pois as instruções sql serão compiladas e armazenadas no servidor DataBase


1

Me deparei com uma série de limitações relacionadas à declaração preparada:

  1. As instruções preparadas são armazenadas em cache somente dentro da mesma sessão (Postgres), portanto, ele realmente funcionará apenas com o pool de conexões
  2. Muitas instruções preparadas diferentes, conforme propostas pelo @BalusC, podem causar um excesso de cache e as instruções em cache anteriores serão descartadas
  3. A consulta deve ser otimizada e usar índices. Parece óbvio, no entanto, por exemplo, a declaração ANY (ARRAY ...) proposta por @Boris em uma das principais respostas não pode usar índices e a consulta será lenta, apesar do cache
  4. A instrução preparada também armazena em cache o plano de consulta e os valores reais de quaisquer parâmetros especificados na instrução não estão disponíveis.

Entre as soluções propostas, eu escolheria aquela que não diminui o desempenho da consulta e faz o menor número de consultas. Esse será o número 4 (enviando poucas consultas) do link @Don ou especificando valores NULL para '?' Desnecessários marcas como proposto por @Vladimir Dyuzhev


1

Acabei de elaborar uma opção específica do PostgreSQL para isso. É um pouco complicado, e vem com seus próprios prós, contras e limitações, mas parece funcionar e não está limitado a uma linguagem de desenvolvimento, plataforma ou driver PG específico.

O truque, é claro, é encontrar uma maneira de passar uma coleção arbitrária de valores como um único parâmetro e fazer com que o banco de dados reconheça isso como vários valores. A solução que eu estou trabalhando é construir uma string delimitada a partir dos valores da coleção, passar essa string como um único parâmetro e usar string_to_array () com a conversão necessária para o PostgreSQL utilizá-la corretamente.

Portanto, se você deseja procurar por "foo", "blá" e "abc", concatená-los juntos em uma única string como: 'foo, blá, abc'. Aqui está o SQL direto:

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

Você obviamente alteraria a conversão explícita para o que você deseja que sua matriz de valores resultante seja - int, texto, uuid etc. também), você pode passá-lo como parâmetro em uma instrução preparada:

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

Isso é flexível o suficiente para suportar coisas como comparações LIKE:

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

Novamente, sem dúvida é um hack, mas funciona e permite que você ainda use instruções preparadas pré-compiladas que usam parâmetros distintos * ahem * , com os benefícios de segurança e (talvez) desempenho associados. É aconselhável e realmente tem desempenho? Naturalmente, isso depende, pois você tem a análise de string e a transmissão possível antes mesmo de sua consulta ser executada. Se você espera enviar três, cinco, algumas dezenas de valores, claro, provavelmente tudo bem. Alguns milhares? Sim, talvez não tanto. YMMV, limitações e exclusões se aplicam, nenhuma garantia expressa ou implícita.

Mas funciona.


0

Apenas para ser completo: contanto que o conjunto de valores não seja muito grande, você também pode simplesmente construir uma string como uma instrução como

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

que você pode passar para preparar () e usar setXXX () em um loop para definir todos os valores. Isso parece nojento, mas muitos "grandes" sistemas comerciais fazem esse tipo de coisa rotineiramente até atingirem limites específicos do banco de dados, como 32 KB (acho que é) para declarações no Oracle.

É claro que você precisa garantir que o conjunto nunca seja excessivamente grande ou faça uma captura de erros no caso em que é.


Sim, você está certo. Meu objetivo nesse caso era reutilizar o PreparedStatement com diferentes números de itens a cada vez.
9118 Chris Mazzola

3
Usar "OU" ofuscaria a intenção. Fique com "IN", pois é mais fácil de ler e a intenção é mais clara. O único motivo para mudar é se os planos de consulta eram diferentes.
James Schek 9/10/08

0

Seguindo a ideia de Adam. Faça com que sua declaração preparada selecione minha_coluna de minha_tabela, onde pesquisa_coluna em (#) Crie uma String x e ​​preencha-a com um número de "?,?,?" dependendo da sua lista de valores Em seguida, basta alterar o # na consulta para sua nova String x e ​​preencher


0

Gere a string de consulta no PreparedStatement para que um número de? Corresponda ao número de itens em sua lista. Aqui está um exemplo:

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
Não há mais necessidade de usar o StringBuilder. O compilador converte os sinais + em StringBuilder.append () de qualquer maneira, para que não ocorra desempenho. Tente você mesmo :)
neu242 18/12/2009

5
@ neu242: Ah, sim, o compilador usa StringBuilder. Mas não da maneira que você pensa. Ao descompilar, generateQsForIné possível ver que duas novas iterações por loop StringBuildersão alocadas e toStringsão chamadas em cada uma. A StringBuilderotimização captura apenas coisas como, "x" + i+ "y" + jmas não se estende além de uma expressão.
AH

@ neu242 Não é possível usar em ps.setObject(1,items)vez de iterar na lista e depois definir o paramteres?
Neha Choudhary

0

Existem diferentes abordagens alternativas que podemos usar para a cláusula IN no PreparedStatement.

  1. Usando consultas únicas - desempenho mais lento e uso intensivo de recursos
  2. Usando StoredProcedure - Mais rápido, mas específico do banco de dados
  3. Criando uma consulta dinâmica para PreparedStatement - Bom desempenho, mas não se beneficia do cache, e o PreparedStatement é recompilado toda vez.
  4. Use NULL em consultas PreparedStatement - desempenho ideal, funciona muito bem quando você conhece o limite dos argumentos da cláusula IN. Se não houver limite, você poderá executar consultas em lote. O trecho de código de amostra é;

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

Você pode verificar mais detalhes sobre essas abordagens alternativas aqui .


"Criando uma consulta dinâmica para PreparedStatement - Bom desempenho, mas não se beneficia do cache, e o PreparedStatement é recompilado todas as vezes." armazenar em cache e evitar recompilar é o que faz com que uma instrução preparada tenha um bom desempenho. Portanto, não concordo com sua reivindicação. No entanto, isso impedirá a injeção de SQL, pois você está limitando a entrada concatenada / dinâmica a uma vírgula.
Brandon

Eu concordo com você, no entanto, "Bom desempenho" aqui é para esse cenário específico. É melhor desempenho do que a abordagem 1, no entanto, a abordagem 2 é mais rápida.
Pankaj

0

Para algumas situações, o regexp pode ajudar. Aqui está um exemplo que verifiquei no Oracle e funciona.

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

Mas há várias desvantagens:

  1. Qualquer coluna aplicada deve ser convertida em varchar / char, pelo menos implicitamente.
  2. Precisa ter cuidado com caracteres especiais.
  3. Ele pode diminuir o desempenho - no meu caso, a versão IN usa a verificação de índice e intervalo, e a versão REGEXP faz a verificação completa.

0

Depois de examinar várias soluções em diferentes fóruns e não encontrar uma boa solução, considero o hack abaixo que eu criei, o mais fácil de seguir e codificar:

Exemplo: suponha que você tenha vários parâmetros para passar na cláusula 'IN'. Basta colocar uma String fictícia dentro da cláusula 'IN', por exemplo, "PARAM" denota a lista de parâmetros que virão no lugar dessa String fictícia.

    select * from TABLE_A where ATTR IN (PARAM);

Você pode coletar todos os parâmetros em uma única variável String no seu código Java. Isso pode ser feito da seguinte forma:

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

Você pode anexar todos os seus parâmetros separados por vírgulas em uma única variável String, 'param1', no nosso caso.

Após coletar todos os parâmetros em uma única String, você pode simplesmente substituir o texto fictício em sua consulta, ou seja, "PARAM" nesse caso, pelo parâmetro String, ou seja, param1. Aqui está o que você precisa fazer:

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

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

Agora você pode executar sua consulta usando o método executeQuery (). Apenas certifique-se de que você não tenha a palavra "PARAM" na sua consulta em nenhum lugar. Você pode usar uma combinação de caracteres especiais e alfabetos em vez da palavra "PARAM" para garantir que não exista a possibilidade de uma palavra aparecer na consulta. Espero que você tenha a solução.

Nota: Embora essa não seja uma consulta preparada, ela faz o trabalho que eu queria que meu código fizesse.


0

Só por completude e porque não vi mais ninguém sugerir:

Antes de implementar qualquer uma das sugestões complicadas acima, considere se a injeção de SQL é realmente um problema no seu cenário.

Em muitos casos, o valor fornecido para IN (...) é uma lista de IDs que foram gerados de maneira a garantir que nenhuma injeção seja possível ... (por exemplo, os resultados de uma seleção anterior, some_id de some_table em que alguma_condição.)

Se for esse o caso, você pode apenas concatenar esse valor e não usar os serviços ou a instrução preparada para ele ou usá-los para outros parâmetros desta consulta.

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

0

PreparedStatement não fornece uma boa maneira de lidar com a cláusula SQL IN. Por http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Você não pode substituir itens que devem se tornar parte da instrução SQL. Isso é necessário porque, se o próprio SQL puder mudar, o o driver não pode pré-compilar a instrução. Ele também tem o bom efeito colateral de impedir ataques de injeção SQL ". Acabei usando a seguinte abordagem:

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

O SetArray é a melhor solução, mas não está disponível para muitos drivers mais antigos. A seguinte solução alternativa pode ser usada no 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);
}

Essa solução é melhor que outras soluções feias de loop em que a string de consulta é criada por iterações manuais


0

Isso funcionou para mim (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)";
}

especificar ligação:

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

Meu exemplo para bancos de dados SQLite e Oracle.

O primeiro loop For é para criar objetos PreparedStatement.

O segundo loop For é para Fornecer valores para parâmetros 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

Minha solução 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 é a matriz que contém suas entradas / chaves / campos, etc.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.