PreparedStatement IN cláusula alternativas?


343

¿Cuáles son las mejores soluciones para usar una INcláusula SQL con instancias de java.sql.PreparedStatement, que no es compatible con valores múltiples debido a problemas de seguridad de ataque de inyección SQL? Un ?marcador de posición representa un valor, en lugar de una lista de valores.

Considere la siguiente instrucción SQL:

SELECT my_column FROM my_table where search_column IN (?)

El uso preparedStatement.setString( 1, "'A', 'B', 'C'" );es esencialmente un intento que no funciona en una solución de las razones para usar ?en primer lugar.

¿Qué soluciones hay disponibles?


1
Oscar, creo que la generación dinámica de (?,?, ....) es la solución más simple si necesitas una cláusula IN, pero la dejé para llamadas individuales ya que el rendimiento fue suficiente en mi caso específico.
Chris Mazzola

66
Una de las ventajas de las declaraciones preparadas es que sohuld se puede compilar una vez para mayor eficiencia. Al hacer dinámica la cláusula in, esto efectivamente niega la declaración preparada.

2
En realidad, esto funciona para MySQL (usando setObject para establecer una matriz de String como el valor del parámetro). ¿Qué DB estás usando?
Frans


Aquí hay una pregunta relacionada: stackoverflow.com/q/6956025/521799
Lukas Eder

Respuestas:


194

Un análisis de las diversas opciones disponibles, y los pros y los contras de cada uno está disponible aquí .

Las opciones sugeridas son:

  • Prepárelo SELECT my_column FROM my_table WHERE search_column = ?, ejecútelo para cada valor y UNIONE los resultados del lado del cliente. Requiere solo una declaración preparada. Lento y doloroso
  • Preparar SELECT my_column FROM my_table WHERE search_column IN (?,?,?) y ejecútalo. Requiere una declaración preparada por tamaño de lista IN. Rápido y obvio.
  • Prepáralo SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...y ejecútalo. [O usar UNION ALLen lugar de esos puntos y comas. --ed] Requiere una declaración preparada por tamaño de lista IN. Estúpidamente lento, estrictamente peor que WHERE search_column IN (?,?,?), así que no sé por qué el blogger incluso lo sugirió.
  • Use un procedimiento almacenado para construir el conjunto de resultados.
  • Prepare N consultas de diferentes tamaños de lista; digamos, con 2, 10 y 50 valores. Para buscar una lista IN con 6 valores diferentes, complete la consulta de tamaño 10 para que se vea SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Cualquier servidor decente optimizará los valores duplicados antes de ejecutar la consulta.

Sin embargo, ninguna de estas opciones es súper genial.

Se han respondido preguntas duplicadas en estos lugares con alternativas igualmente sensatas, pero ninguna de ellas es súper genial:

La respuesta correcta, si está utilizando JDBC4 y un servidor compatible x = ANY(y), es usar PreparedStatement.setArraycomo se describe aquí:

Sin embargo, no parece haber ninguna manera de hacer que setArrayfuncione con las listas IN.


A veces, las instrucciones SQL se cargan en tiempo de ejecución (por ejemplo, desde un archivo de propiedades) pero requieren un número variable de parámetros. En tales casos, primero defina la consulta:

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

A continuación, cargue la consulta. Luego determine el número de parámetros antes de ejecutarlo. Una vez que se conoce el recuento de parámetros, ejecute:

sql = any( sql, count );

Por ejemplo:

/**
 * 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 ciertas bases de datos donde no se admite pasar una matriz a través de la especificación JDBC 4, este método puede facilitar la transformación de la condición de cláusula lenta = ?a la más rápida IN (?), que luego se puede ampliar llamando al anymétodo.


123

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

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
se ve bien. ¿Qué parte de este código es específico de PostreSQL? el "donde search_column = CUALQUIER (?)"? o la conexión.createArrayOf? ¿o algo mas?
David Portabella

1
Creo que es más específico de JDBC4 que específico de PostgreSQL, debido a la .createArrayOf()parte, pero no estoy seguro de que la semántica estricta para los usuarios Arrayesté definida por la especificación JDBC.
lvella

3
Si .createArrayOfno funciona, puede hacer su propia creación manual de matriz literal como String arrayLiteral = "{A,\"B \", C,D}" (tenga en cuenta que "B" tiene un espacio mientras que C no) y luego statement.setString(1,arrayLiteral)dónde está la instrucción preparada ... IN (SELECT UNNEST(?::VARCHAR[]))o ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PD: no creo que ANYfuncione con a SELECT.)
ADTC

¡Gran solución! Realmente me salvó el día. Para la matriz entera utilicé "int" en el primer parámetro de createArrayOf () y se ve bien. Sin embargo, ese primer parámetro parece específico de DB, basado en la documentación.
Emmanuel Touzery

2
Esta parece la solución más limpia. Si alguien está buscando la sintaxis específica de HSQLDB: logré hacer que esto funcione con IN (UNNEST (?))
aureianimus

19

No hay manera simple AFAIK. Si el objetivo es mantener alta la relación de caché de la declaración (es decir, no crear una declaración por cada recuento de parámetros), puede hacer lo siguiente:

  1. cree una declaración con algunos (por ejemplo, 10) parámetros:

    ... DONDE UN IN (?,?,?,?,?,?,?,?,?,?) ...

  2. Vincula todos los parámetros actuales

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

  3. Vincula el resto como NULL

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

NULL nunca coincide con nada, por lo que el generador de planes SQL lo optimiza.

La lógica es fácil de automatizar cuando pasa una Lista a una función 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 coincide con nada" - ¿ NULLEn la consulta coincidiría con un NULLvalor en la base de datos?
Craig McQueen

55
@CraigMcQueen No, no lo haría. Nulo ni siquiera coincide con nulo, según el estándar ANSI.
Dawood ibn Kareem

Puede hacer coincidir NULL utilizando la palabra clave IS NULL. Una buena manera de detectar filas que no existen en la tabla unida es utilizar una UNIDAD IZQUIERDA junto con IS NULL. 'SELECCIONE a.URL, b.URL DE LA TABLA_A a IZQUIERDA UNIRSE A LA TABLA_B b EN a_A.URL = b_B.URL DONDE b.URL ES NULO' Esto mostrará todas las filas de la tabla A que no coinciden en la tabla B.
Jens Tandstad

3
Pero ten cuidado con esto. NOT INy INno maneje nulos de la misma manera. Ejecute esto y vea qué sucede: select 'Matched' as did_it_match where 1 not in (5, null); luego quite el nully observe la magia.
Brandon

O puede establecer todos los parámetros adicionales en el valor de cualquier parámetro anterior. Cualquier motor DB decente los filtrará. Entonces a IN (1,2,3,3,3,3,3)es lo mismo que a IN (1,2,3). También funciona con NOT INunlike a NOT IN (1,2,3,null,null,null,null)(que siempre no devuelve filas, ya any_value != NULLque siempre es falso).
Ruslan Stelmachenko

11

Una solución desagradable, pero ciertamente factible es usar una consulta anidada. Cree una tabla temporal MYVALUES con una columna en ella. Inserte su lista de valores en la tabla MYVALUES. Luego ejecuta

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

Feo, pero una alternativa viable si su lista de valores es muy grande.

Esta técnica tiene la ventaja adicional de tener planes de consulta potencialmente mejores desde el optimizador (verifique en una página los valores múltiples, el escaneo de tablas solo una vez, una vez por valor, etc.) puede ahorrar en gastos generales si su base de datos no almacena en caché las declaraciones preparadas. Sus "INSERTOS" tendrían que hacerse por lotes y la tabla MYVALUES podría necesitar ajustes para tener un bloqueo mínimo u otras protecciones de alto gasto.


¿Qué ventajas tendría eso sobre consultar my_table un valor a la vez?
Paul Tomblin

3
El optimizador de consultas puede reducir la carga de E / S al recuperar todas las coincidencias posibles de una página cargada. Se pueden realizar escaneos de tabla o escaneos de índice una vez en lugar de una vez por valor. Los gastos generales para insertar valores se pueden reducir con operaciones por lotes y pueden ser inferiores a varias consultas.
James Schek

1
se ve bien, pero podría haber problemas con la concurrencia. ¿La especificación jdbc contiene una forma de crear una tabla temporal anónima en la memoria? o algo así, si es posible no jdbc-vendor específico?
David Portabella

9

Las limitaciones del operador in () son la raíz de todo mal.

Funciona para casos triviales, y puede extenderlo con "generación automática de la declaración preparada", sin embargo, siempre tiene sus límites.

  • si está creando una declaración con un número variable de parámetros, eso generará una sobrecarga de análisis sql en cada llamada
  • en muchas plataformas, el número de parámetros del operador in () es limitado
  • en todas las plataformas, el tamaño total del texto SQL es limitado, lo que hace imposible enviar 2000 marcadores de posición para los parámetros
  • no es posible enviar variables de enlace de 1000-10k, ya que el controlador JDBC tiene sus limitaciones

El enfoque in () puede ser lo suficientemente bueno para algunos casos, pero no a prueba de cohetes :)

La solución a prueba de cohetes es pasar el número arbitrario de parámetros en una llamada separada (pasando un par de parámetros, por ejemplo), y luego tener una vista (o cualquier otra forma) para representarlos en SQL y usarlos en su criterios

Una variante de fuerza bruta está aquí http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Sin embargo, si puede usar PL / SQL, este desastre puede volverse bastante bueno.

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;

Luego puede pasar un número arbitrario de identificadores de cliente separados por comas en el parámetro y:

  • no obtendrá un retraso de análisis, ya que el SQL para select es estable
  • sin complejidad de funciones canalizadas: es solo una consulta
  • el SQL está usando una unión simple, en lugar de un operador IN, que es bastante rápido
  • después de todo, es una buena regla general no golpear la base de datos con ninguna selección simple o DML, ya que es Oracle, que ofrece años luz de más que MySQL o motores de bases de datos simples similares. PL / SQL le permite ocultar el modelo de almacenamiento del modelo de dominio de su aplicación de manera efectiva.

El truco aquí es:

  • necesitamos una llamada que acepte la cadena larga y la almacene en algún lugar donde la sesión db pueda acceder a ella (por ejemplo, variable de paquete simple o dbms_session.set_context)
  • entonces necesitamos una vista que pueda analizar esto en filas
  • y luego tiene una vista que contiene los identificadores que está consultando, por lo que todo lo que necesita es una simple unión a la tabla consultada.

La vista se ve así:

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

donde aux_in_list.getpayload se refiere a la cadena de entrada original.


Un enfoque posible sería pasar matrices pl / sql (solo compatible con Oracle), sin embargo, no puede usarlas en SQL puro, por lo tanto, siempre se necesita un paso de conversión. La conversión no se puede hacer en SQL, así que, después de todo, pasar un clob con todos los parámetros en cadena y convertirlo dentro de una vista es la solución más eficiente.


6

Así es como lo resolví en mi propia aplicación. Idealmente, debe usar un StringBuilder en lugar 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 una variable como x arriba en lugar de números concretos ayuda mucho si decides cambiar la consulta más adelante.


intentó lo mismo, no funciona para Oracle
Santosh Raviteja

5

Nunca lo he intentado, pero ¿haría .setArray () hacer lo que estás buscando?

Actualización : evidentemente no. setArray solo parece funcionar con un java.sql.Array que proviene de una columna ARRAY que ha recuperado de una consulta anterior, o una subconsulta con una columna ARRAY.


44
No funciona con todas las bases de datos, pero es el enfoque "correcto".
skaffman

Te refieres a todos los conductores. Algunos controladores tienen equivalentes propietarios del estándar de este año (¿del siglo pasado?). Otra forma es agrupar un lote de valores en una tabla temporal, pero no todas las bases de datos admiten que ...
Tom Hawtin - línea de ataque

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Según Sun, el contenido de la matriz [típicamente] permanece en el lado del servidor y se extrae según sea necesario. PreparedStatement.setArray () puede enviar una matriz desde un ResultSet anterior, no crear una nueva matriz en el lado del cliente.
Chris Mazzola

5

Mi solución es:

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

Ahora puede usar una variable para obtener algunos valores en una tabla:

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

Entonces, la declaración preparada podría ser:

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

Saludos,

Javier Ibáñez


Esto es PL / SQL, sí. No funcionará en otras bases de datos. Tenga en cuenta que esta implementación tiene una limitación de parámetros de entrada (la longitud total está limitada a 32k caracteres), así como una limitación de rendimiento ya que la llamada a la función canalizada hace un cambio de contexto entre los motores PL / SQL y SQL de Oracle.
Gee Bee

3

Supongo que podría (utilizando la manipulación básica de cadenas) generar la cadena de consulta en el PreparedStatementpara tener un número de ?coincidencias con el número de elementos en su lista.

Por supuesto, si está haciendo eso, está a solo un paso de generar un gigante encadenado ORen su consulta, pero sin tener el número correcto ?en la cadena de consulta, no veo cómo puede solucionar esto.


¿No es realmente una solución para mí ya que quiero enviar un número diferente de? cada vez que llamo al ps. Pero no pienses que no lo había considerado. : P
Chris Mazzola

44
Otro truco: puede usar una gran cantidad de marcadores de posición de parámetros, tantos como la lista más larga de valores que tendrá, y si su lista de valores es más corta, puede repetir los valores: ... ¿DÓNDE ENTRADA el campo de búsqueda (? ,?,?,?,?,?,?,?) y luego proporcione valores: A, B, C, D, A, B, C, D
Bill Karwin

1
Pero, en general, estoy a favor de la solución de Adam: ¿generar el SQL dinámicamente y concatenar? marcadores de posición para que coincida con el número de valores que tiene que pasar.
Bill Karwin

Bill, esa solución es viable si no quiero reutilizar la Declaración Preparada. Otra solución es hacer que la única llamada de parámetro varias veces y acumular los resultados en el lado del cliente. Probablemente sería más eficiente construir / ejecutar una nueva Declaración con un número personalizado de? aunque cada vez.
Chris Mazzola

3

Puede usar el método setArray como se menciona en este 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
esto no es compatible con todos los controladores, si la característica no es compatible, obtendrá SQLFeatureNotSupportedException
sin nombre el

Lamentablemente, mi controlador no lo admite
EdXX

Esto no funciona para Oracle
Santosh Raviteja

3

Puede usar Collections.nCopiespara generar una colección de marcadores de posición y unirlos 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 la mejor solución hasta ahora cuando se usa Oracle JDBC ...
jansohn

2

Aquí hay una solución completa en Java para crear la declaración preparada para usted:

/*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 permite pasar java.util.Lists a NamedParameterJdbcTemplate , que automatiza la generación de (?,?,?, ...,?), corresponda para la cantidad de argumentos.

Para Oracle, esta publicación de blog discute el uso de oracle.sql.ARRAY (Connection.createArrayOf no funciona con Oracle). Para esto, debe modificar su declaración SQL:

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

La función de tabla oracle transforma la matriz pasada en una tabla como valor utilizable en la INdeclaración.


1

intente usar la función instr?

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

entonces

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

Es cierto que este es un truco un poco sucio, pero reduce las oportunidades para la inyección de sql. Funciona en Oracle de todos modos.


Ah, y soy consciente de que no utilizará índices
stjohnroe

no funcionaría para algunas cadenas, por ejemplo, si la cadena contiene un ','.
David Portabella

1

Sormula admite el operador SQL IN al permitirle proporcionar un objeto java.util.Collection como parámetro. Crea una declaración preparada con un? para cada uno de los elementos de la colección. Vea el Ejemplo 4 (SQL en el ejemplo es un comentario para aclarar lo que se crea pero no es usado por Sormula).


1

En lugar de usar

SELECT my_column FROM my_table where search_column IN (?)

use la Declaración SQL como

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

y

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

o use un procedimiento almacenado, esta sería la mejor solución, ya que las instrucciones sql se compilarán y almacenarán en el servidor DataBase


1

Encontré una serie de limitaciones relacionadas con la declaración preparada:

  1. Las declaraciones preparadas se almacenan en caché solo dentro de la misma sesión (Postgres), por lo que realmente funcionará solo con la agrupación de conexiones
  2. Muchas declaraciones preparadas diferentes propuestas por @BalusC pueden hacer que el caché se llene en exceso y las declaraciones almacenadas previamente en caché se descartarán
  3. La consulta tiene que ser optimizada y usar índices. Suena obvio, sin embargo, por ejemplo, la declaración ANY (ARRAY ...) propuesta por @Boris en una de las respuestas principales no puede usar índices y la consulta será lenta a pesar del almacenamiento en caché
  4. La declaración preparada también almacena en caché el plan de consulta y los valores reales de cualquier parámetro especificado en la declaración no están disponibles.

Entre las soluciones propuestas, elegiría la que no disminuye el rendimiento de la consulta y hace el menor número de consultas. Este será el # 4 (agrupando pocas consultas) del enlace @Don o especificando valores NULL para '?' Innecesarios marcas propuestas por @Vladimir Dyuzhev


1

Acabo de elaborar una opción específica de PostgreSQL para esto. Es un truco y viene con sus propios pros, contras y limitaciones, pero parece funcionar y no se limita a un lenguaje de desarrollo específico, plataforma o controlador PG.

El truco, por supuesto, es encontrar una manera de pasar una colección arbitraria de valores de longitud como un solo parámetro, y hacer que db lo reconozca como valores múltiples. La solución que tengo trabajando es construir una cadena delimitada a partir de los valores de la colección, pasar esa cadena como un solo parámetro y usar string_to_array () con la conversión necesaria para que PostgreSQL lo use correctamente.

Entonces, si desea buscar "foo", "blah" y "abc", puede concatenarlos en una sola cadena como: 'foo, blah, abc'. Aquí está el SQL directo:

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

Obviamente, cambiaría la conversión explícita a lo que quisiera que fuera su matriz de valores resultante: int, text, uuid, etc. Y porque la función está tomando un solo valor de cadena (o dos, supongo, si desea personalizar el delimitador) también), puede pasarlo como parámetro en una declaración preparada:

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

Esto es incluso lo suficientemente flexible como para soportar cosas como comparaciones LIKE:

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

De nuevo, no hay duda de que es un truco, pero funciona y le permite seguir usando declaraciones preparadas previamente compiladas que toman * ejem * parámetros discretos, con los beneficios de seguridad y (tal vez) de rendimiento que lo acompañan. ¿Es aconsejable y realmente eficaz? Naturalmente, depende, ya que tiene el análisis de cadenas y posiblemente la conversión antes de que se ejecute su consulta. Si espera enviar tres, cinco, algunas docenas de valores, seguro, probablemente esté bien. Unos pocos miles? Sí, tal vez no tanto. YMMV, se aplican limitaciones y exclusiones, sin garantía expresa o implícita.

Pero funciona.


0

Solo para completar: siempre que el conjunto de valores no sea demasiado grande, también puede simplemente construir una declaración como

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

que luego puede pasar para preparar (), y luego usar setXXX () en un bucle para establecer todos los valores. Esto parece asqueroso, pero muchos sistemas comerciales "grandes" rutinariamente hacen este tipo de cosas hasta que alcanzan los límites específicos de DB, como 32 KB (creo que es) para las declaraciones en Oracle.

Por supuesto, debe asegurarse de que el conjunto nunca será excesivamente grande o hacer un error de captura en caso de que lo sea.


Sí tienes razón. Mi objetivo en este caso era reutilizar la declaración preparada con diferentes números de elementos cada vez.
Chris Mazzola

3
Usar "OR" ofuscaría la intención. Quédese con "IN" ya que es más fácil de leer y la intención es más clara. La única razón para cambiar es si los planes de consulta eran diferentes.
James Schek

0

Siguiendo la idea de Adam. Haga que su declaración preparada seleccione my_column de my_table donde search_column en (#) Cree una Cadena x y complétela con un número de "?,?,?" dependiendo de su lista de valores Luego simplemente cambie el # en la consulta para su nueva Cadena x un poblado


0

Genere la cadena de consulta en PreparedStatement para que un número de? Coincida con el número de elementos en su lista. Aquí hay un ejemplo:

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

55
Ya no es necesario usar StringBuilder. El compilador convierte los signos + a StringBuilder.append () de todos modos, por lo que no hay impacto en el rendimiento. Pruébalo :)
neu242

55
@ neu242: Oh, sí, el compilador usa StringBuilder. Pero no en la forma en que piensas. Al descompilar generateQsForIn, puede ver que por iteración de bucle se asignan dos nuevos StringBuildery toStringse llama a cada uno. La StringBuilderoptimización solo captura cosas como, "x" + i+ "y" + jpero no se extiende más allá de una expresión.
AH

@ neu242 ¿No puede usar en ps.setObject(1,items)lugar de iterar sobre la lista y luego configurar el paramteres?
Neha Choudhary

0

Existen diferentes enfoques alternativos que podemos usar para la cláusula IN en PreparedStatement.

  1. Uso de consultas individuales: rendimiento más lento y uso intensivo de recursos
  2. Uso de StoredProcedure: el más rápido pero específico de la base de datos
  3. Creación de consultas dinámicas para PreparedStatement: buen rendimiento, pero no obtiene el beneficio del almacenamiento en caché, y PreparedStatement se recompila cada vez.
  4. Use NULL en consultas PreparedStatement: rendimiento óptimo, funciona muy bien cuando conoce el límite de los argumentos de la cláusula IN. Si no hay límite, puede ejecutar consultas en lote. El fragmento de código de muestra es;

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

Puede consultar más detalles sobre estos enfoques alternativos aquí .


"Crear una consulta dinámica para PreparedStatement: buen rendimiento pero no obtiene el beneficio del almacenamiento en caché y PreparedStatement se recompila cada vez". el almacenamiento en caché y evitar las recompilaciones es lo que hace que una declaración preparada funcione bien. Por lo tanto, no estoy de acuerdo con su reclamo. Sin embargo, esto evitará la inyección de SQL ya que está limitando la entrada concatenada / dinámica a una coma.
Brandon

Estoy de acuerdo con usted, sin embargo, "Buen rendimiento" aquí es para este escenario específico. Es mejor rendimiento que el enfoque 1, sin embargo, el enfoque 2 es el más rápido.
Pankaj

0

Para algunas situaciones, regexp podría ayudar. Aquí hay un ejemplo que he comprobado en Oracle, y funciona.

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

Pero hay una serie de inconvenientes:

  1. Cualquier columna que aplique debe convertirse a varchar / char, al menos implícitamente.
  2. Hay que tener cuidado con los caracteres especiales.
  3. Puede ralentizar el rendimiento: en mi caso, la versión IN usa exploración de índice y rango, y la versión REGEXP realiza exploración completa.

0

Después de examinar varias soluciones en diferentes foros y no encontrar una buena solución, creo que el siguiente truco que se me ocurrió es el más fácil de seguir y codificar:

Ejemplo: suponga que tiene que pasar varios parámetros en la cláusula 'IN'. Simplemente coloque una Cadena ficticia dentro de la cláusula 'IN', diga, "PARAM" denote la lista de parámetros que vendrán en lugar de esta Cadena ficticia.

    select * from TABLE_A where ATTR IN (PARAM);

Puede recopilar todos los parámetros en una sola variable de cadena en su código Java. Esto puede hacerse de la siguiente manera:

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

Puede agregar todos sus parámetros separados por comas en una sola variable de cadena, 'param1', en nuestro caso.

Después de recopilar todos los parámetros en una sola Cadena, puede reemplazar el texto ficticio en su consulta, es decir, "PARAM" en este caso, con el parámetro Cadena, es decir, param1. Aquí está lo que tú necesitas hacer:

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

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

Ahora puede ejecutar su consulta utilizando el método executeQuery (). Solo asegúrese de no tener la palabra "PARAM" en su consulta en ninguna parte. Puede usar una combinación de caracteres especiales y alfabetos en lugar de la palabra "PARAM" para asegurarse de que no haya posibilidad de que dicha palabra aparezca en la consulta. Espero que hayas encontrado la solución.

Nota: Aunque esta no es una consulta preparada, hace el trabajo que quería que hiciera mi código.


0

Solo por completo y porque no vi a nadie más sugerirlo:

Antes de implementar cualquiera de las complicadas sugerencias anteriores, considere si la inyección SQL es realmente un problema en su escenario.

En muchos casos, el valor proporcionado a IN (...) es una lista de identificadores que se han generado de manera que pueda estar seguro de que no es posible la inyección ... (por ejemplo, los resultados de una selección previa de some_id de some_table donde alguna_condición.)

Si ese es el caso, puede concatenar este valor y no usar los servicios o la declaración preparada para él o usarlos para otros parámetros de esta consulta.

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

0

PreparedStatement no proporciona ninguna buena manera de lidiar con la cláusula SQL IN. De acuerdo con http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "No puede sustituir cosas que están destinadas a formar parte de la instrucción SQL. Esto es necesario porque si el SQL mismo puede cambiar, el el controlador no puede precompilar la declaración. También tiene el agradable efecto secundario de prevenir ataques de inyección SQL ". Terminé usando el siguiente enfoque:

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 es la mejor solución, pero no está disponible para muchos controladores antiguos. La siguiente solución alternativa se puede utilizar en 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);
}

Esta solución es mejor que otras soluciones feas mientras que las cadenas de consulta se construyen mediante iteraciones manuales.


0

Esto funcionó para mí (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)";
}

especifique vinculante:

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

Mi ejemplo para las bases de datos SQLite y Oracle.

El primer bucle For es para crear un objeto PreparedStatement.

El segundo bucle For es para suministrar valores para parámetros de declaración preparada.

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

Mi solución 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 es la matriz que contiene su entrada / claves / campos, etc.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.