Alternatives à la clause PreparedStatement IN?


343

Quelles sont les meilleures solutions de contournement pour utiliser une INclause SQL avec des instances de java.sql.PreparedStatement, qui n'est pas prise en charge pour plusieurs valeurs en raison de problèmes de sécurité d'attaque par injection SQL: un ?espace réservé représente une valeur, plutôt qu'une liste de valeurs.

Considérez l'instruction SQL suivante:

SELECT my_column FROM my_table where search_column IN (?)

L'utilisation preparedStatement.setString( 1, "'A', 'B', 'C'" );est essentiellement une tentative non fonctionnelle de contourner les raisons de l'utilisation ?en premier lieu.

Quelles solutions de contournement sont disponibles?


1
Oscar, je pense que la génération dynamique de (?,?, ....) est la solution de contournement la plus simple si vous avez besoin d'une clause IN, mais je l'ai laissé aux appels individuels car les performances étaient suffisantes dans mon cas spécifique.
Chris Mazzola

6
L'un des avantages des instructions préparées est que sohuld peut être compilé une fois pour plus d'efficacité. En dynamisant la clause in, cela annule efficacement l'instruction préparée.

2
En fait, cela fonctionne pour MySQL (en utilisant setObject pour définir un tableau de String comme valeur de paramètre). Quelle base de données utilisez-vous?
Frans


Voici une question connexe: stackoverflow.com/q/6956025/521799
Lukas Eder

Réponses:


194

Une analyse des différentes options disponibles et les avantages et inconvénients de chacune sont disponibles ici .

Les options suggérées sont:

  • Préparez-le SELECT my_column FROM my_table WHERE search_column = ?, exécutez-le pour chaque valeur et UNIONnez les résultats côté client. Nécessite une seule déclaration préparée. Lente et douloureuse.
  • Préparez-le SELECT my_column FROM my_table WHERE search_column IN (?,?,?)et exécutez-le. Nécessite une instruction préparée par taille de liste IN. Rapide et évident.
  • Préparez-le SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...et exécutez-le. [Ou utilisez UNION ALLà la place de ces points-virgules. --ed] Nécessite une instruction préparée par taille de liste IN. Bêtement lent, strictement pire que WHERE search_column IN (?,?,?), donc je ne sais pas pourquoi le blogueur l'a même suggéré.
  • Utilisez une procédure stockée pour construire l'ensemble de résultats.
  • Préparez N requêtes de taille de liste IN différentes; disons, avec 2, 10 et 50 valeurs. Pour rechercher une liste IN avec 6 valeurs différentes, remplissez la requête de taille 10 pour qu'elle ressemble SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Tout serveur décent optimisera les valeurs en double avant d'exécuter la requête.

Cependant, aucune de ces options n'est super géniale.

Des réponses ont été apportées à ces questions en double avec des alternatives tout aussi sensées, mais aucune d'entre elles n'est super géniale:

La bonne réponse, si vous utilisez JDBC4 et un serveur qui prend en charge x = ANY(y), est d'utiliser PreparedStatement.setArraycomme décrit ici:

Cependant, il ne semble pas y avoir de moyen de setArraytravailler avec les listes IN.


Parfois, les instructions SQL sont chargées lors de l'exécution (par exemple, à partir d'un fichier de propriétés) mais nécessitent un nombre variable de paramètres. Dans de tels cas, définissez d'abord la requête:

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

Ensuite, chargez la requête. Déterminez ensuite le nombre de paramètres avant de l'exécuter. Une fois le nombre de paramètres connu, exécutez:

sql = any( sql, count );

Par exemple:

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

Pour certaines bases de données où le passage d'un tableau via la spécification JDBC 4 n'est pas pris en charge, cette méthode peut faciliter la transformation de la condition slow = ?en IN (?)clause clause plus rapide , qui peut ensuite être développée en appelant la anyméthode.


123

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

1
Cela semble bon. quelle partie de ce code est spécifique à PostreSQL? le "où search_column = TOUT (?)"? ou le fichier connection.createArrayOf? ou autre chose?
David Portabella

1
Je pense qu'il est plus spécifique à JDBC4 qu'à PostgreSQL, à cause de la .createArrayOf()partie, mais je ne suis pas sûr que la sémantique stricte pour les utilisateurs Arraysoit définie par la spécification JDBC.
lvella

3
Si .createArrayOfcela ne fonctionne pas, vous pouvez créer votre propre création manuelle de littéral de tableau comme String arrayLiteral = "{A,\"B \", C,D}" (notez que "B" a un espace tandis que C ne fonctionne pas) et ensuite statement.setString(1,arrayLiteral)où l'instruction préparée est ... IN (SELECT UNNEST(?::VARCHAR[]))ou ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: je ne pense pas que cela ANYfonctionne avec a SELECT.)
ADTC

Excellente solution! Vraiment sauvé la journée pour moi. Pour le tableau entier, j'ai utilisé "int" dans le premier paramètre de createArrayOf () et ça a l'air bien. Ce premier paramètre apparaît cependant spécifique à la base de données, sur la base de la documentation.
Emmanuel Touzery

2
Cela semble la solution la plus propre. Si quelqu'un recherche la syntaxe spécifique HSQLDB: j'ai réussi à faire fonctionner cela avec IN (UNNEST (?))
aureianimus

19

Pas de manière simple AFAIK. Si l'objectif est de maintenir un rapport de cache d'instructions élevé (c'est-à-dire de ne pas créer d'instruction pour chaque nombre de paramètres), vous pouvez procéder comme suit:

  1. créer une instruction avec quelques paramètres (par exemple 10):

    ... O A UN DANS (?,?,?,?,?,?,?,?,?,?) ...

  2. Lier tous les paramètres actuall

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

  3. Lier le reste comme NULL

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

NULL ne correspond jamais à rien, il est donc optimisé par le générateur de plan SQL.

La logique est facile à automatiser lorsque vous passez une liste dans une fonction 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 ne correspond à rien" - NULLLa requête correspondrait-elle à une NULLvaleur dans la base de données?
Craig McQueen

5
@CraigMcQueen Non, ce ne serait pas le cas. Null ne correspond même pas à null, selon la norme ANSI.
Dawood ibn Kareem, le

Vous pouvez faire correspondre NULL à l'aide du mot clé IS NULL. Une bonne façon de détecter les lignes qui n'existent pas dans la table jointe consiste à utiliser un LEFT JOIN avec IS NULL. 'SELECT a.URL, b.URL FROM TABLE_A a LEFT JOIN TABLE_B b ON a_A.URL = b_B.URL WHERE b.URL IS NULL' Ceci affichera toutes les lignes du tableau A qui n'ont aucune correspondance dans le tableau B.
Jens Tandstad

3
Soyez prudent avec cela cependant. NOT INet INne gérez pas les null de la même manière. Exécutez ceci et voyez ce qui se passe: select 'Matched' as did_it_match where 1 not in (5, null); Retirez ensuite le nullet observez la magie.
Brandon

Ou vous pouvez définir tous les paramètres supplémentaires sur la valeur de tout paramètre précédent. Tout moteur DB décent les filtrera. C'est a IN (1,2,3,3,3,3,3)la même chose que a IN (1,2,3). Il fonctionne également avec NOT INcontrairement à a NOT IN (1,2,3,null,null,null,null)(qui ne renvoie toujours aucune ligne car il any_value != NULLest toujours faux).
Ruslan Stelmachenko

11

Une solution de contournement désagréable, mais certainement réalisable consiste à utiliser une requête imbriquée. Créez une table temporaire MYVALUES avec une colonne dedans. Insérez votre liste de valeurs dans la table MYVALUES. Ensuite, exécutez

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

Moche, mais une alternative viable si votre liste de valeurs est très longue.

Cette technique présente l'avantage supplémentaire de plans de requête potentiellement meilleurs de l'optimiseur (vérifier une page pour plusieurs valeurs, tablescan une seule fois au lieu d'une fois par valeur, etc.) peut économiser sur la surcharge si votre base de données ne met pas en cache les instructions préparées. Vos "INSERTS" devront être effectués en lot et la table MYVALUES devra peut-être être modifiée pour avoir un verrouillage minimal ou d'autres protections élevées.


Quels avantages cela aurait-il par rapport à l'interrogation de my_table une valeur à la fois?
Paul Tomblin

3
L'optimiseur de requêtes peut réduire la charge d'E / S en récupérant toutes les correspondances possibles à partir d'une page chargée. Les analyses de tables ou les analyses d'index peuvent être effectuées une fois au lieu d'une fois par valeur. Les frais généraux pour l'insertion de valeurs peuvent être réduits avec des opérations par lots et peuvent être inférieurs à plusieurs requêtes.
James Schek

1
cela semble bon, mais il peut y avoir des problèmes de simultanéité. la spécification jdbc contient-elle un moyen de créer une table anonyme temporelle en mémoire? ou quelque chose comme ça, si possible pas spécifique au fournisseur jdbc?
David Portabella

9

Les limitations de l'opérateur in () sont la racine de tout mal.

Cela fonctionne pour les cas triviaux, et vous pouvez l'étendre avec la "génération automatique de l'instruction préparée", mais elle a toujours ses limites.

  • si vous créez une instruction avec un nombre variable de paramètres, cela fera une surcharge d'analyse sql à chaque appel
  • sur de nombreuses plateformes, le nombre de paramètres de l'opérateur in () est limité
  • sur toutes les plateformes, la taille totale du texte SQL est limitée, ce qui rend impossible l'envoi de 2000 espaces réservés pour les paramètres
  • l'envoi de variables de liaison de 1000 à 10 000 n'est pas possible, car le pilote JDBC a ses limites

L'approche in () peut être assez bonne dans certains cas, mais pas à l'épreuve des fusées :)

La solution à l'épreuve des fusées consiste à transmettre le nombre arbitraire de paramètres dans un appel distinct (en passant un bloc de paramètres, par exemple), puis à avoir une vue (ou toute autre manière) pour les représenter en SQL et les utiliser dans votre où Critères.

Une variante de force brute est ici http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Cependant, si vous pouvez utiliser PL / SQL, ce désordre peut devenir assez soigné.

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;

Ensuite, vous pouvez passer un nombre arbitraire d'ID de client séparés par des virgules dans le paramètre et:

  • n'obtiendra aucun délai d'analyse, car le SQL de select est stable
  • pas de complexité des fonctions en pipeline - c'est juste une requête
  • le SQL utilise une simple jointure, au lieu d'un opérateur IN, ce qui est assez rapide
  • après tout, c'est une bonne règle de base de ne pas frapper la base de données avec un simple select ou DML, car c'est Oracle, qui offre des années-lumière de plus que MySQL ou des moteurs de base de données simples similaires. PL / SQL vous permet de masquer le modèle de stockage de votre modèle de domaine d'application de manière efficace.

L'astuce est la suivante:

  • nous avons besoin d'un appel qui accepte la longue chaîne et stockons quelque part où la session db peut y accéder (par exemple, une simple variable de package ou dbms_session.set_context)
  • alors nous avons besoin d'une vue qui peut analyser cela en lignes
  • et puis vous avez une vue qui contient les identifiants que vous interrogez, donc tout ce dont vous avez besoin est une simple jointure à la table interrogée.

La vue ressemble à:

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

où aux_in_list.getpayload fait référence à la chaîne d'entrée d'origine.


Une approche possible serait de passer des tableaux pl / sql (pris en charge par Oracle uniquement), mais vous ne pouvez pas les utiliser en SQL pur, donc une étape de conversion est toujours nécessaire. La conversion ne peut pas être effectuée en SQL, donc après tout, passer un clob avec tous les paramètres dans une chaîne et le convertir dans une vue est la solution la plus efficace.


6

Voici comment je l'ai résolu dans ma propre application. Idéalement, vous devriez utiliser un StringBuilder au lieu d'utiliser + pour les chaînes.

    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'utilisation d'une variable comme x ci-dessus au lieu de nombres concrets aide beaucoup si vous décidez de modifier la requête ultérieurement.


5

Je ne l'ai jamais essayé, mais .setArray () ferait-il ce que vous cherchez?

Mise à jour : Evidemment non. setArray ne semble fonctionner qu'avec un java.sql.Array provenant d'une colonne ARRAY que vous avez récupérée d'une requête précédente, ou d'une sous-requête avec une colonne ARRAY.


4
Ne fonctionne pas avec toutes les bases de données, mais c'est l'approche "correcte".
skaffman

Vous voulez dire tous les conducteurs. Certains pilotes ont des équivalents propriétaires de cette norme vieille de plusieurs années (siècle dernier?). Une autre façon est de jeter un lot de valeurs dans une table temporaire, mais toutes les bases de données ne prennent pas en charge cela ...
Tom Hawtin - tackline

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Selon Sun, le contenu de la baie [généralement] reste côté serveur et est extrait selon les besoins. PreparedStatement.setArray () peut renvoyer un tableau à partir d'un ensemble de résultats précédent, et non créer un nouveau tableau côté client.
Chris Mazzola

5

Ma solution est la suivante:

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

Vous pouvez maintenant utiliser une variable pour obtenir des valeurs dans une table:

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

Ainsi, la déclaration préparée pourrait être:

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

Cordialement,

Javier Ibanez


C'est PL / SQL, oui. Il ne fonctionnera pas dans d'autres bases de données. Notez que cette implémentation a une limitation des paramètres d'entrée - la longueur totale est limitée à 32k caractères -, ainsi qu'une limitation des performances puisque l'appel à la fonction en pipeline fait un changement de contexte entre PL / SQL et les moteurs SQL d'Oracle.
Gee Bee

3

Je suppose que vous pourriez (en utilisant la manipulation de chaîne de base) générer la chaîne de requête dans le PreparedStatementpour avoir un certain nombre de? correspondant au nombre d'éléments dans votre liste.

Bien sûr, si vous faites cela, vous n'êtes qu'à un pas de générer un géant enchaîné ORdans votre requête, mais sans avoir le bon nombre de ?dans la chaîne de requête, je ne vois pas comment vous pouvez contourner cela.


Pas vraiment une solution pour moi car je veux envoyer un nombre différent de? chaque fois que j'appelle le ps. Mais ne pense pas que je n'y avais pas pensé. : P
Chris Mazzola

4
Autre hack: vous pouvez utiliser un grand nombre d'espaces réservés pour les paramètres - autant que la liste de valeurs la plus longue que vous aurez - et si votre liste de valeurs est plus courte, vous pouvez répéter les valeurs: ... OERE le champ de recherche DANS (? ,?,?,?,?,?,?,?), puis fournissez les valeurs: A, B, C, D, A, B, C, D
Bill Karwin

1
Mais dans l'ensemble, je préfère la solution d'Adam: générer le SQL dynamiquement et concaténer? des espaces réservés pour correspondre au nombre de valeurs que vous devez transmettre.
Bill Karwin

Bill, cette solution est réalisable si je ne veux pas réutiliser le PreparedStatement. Une autre solution consiste à effectuer plusieurs fois le même appel param et à accumuler les résultats côté client. Il serait probablement plus efficace de créer / exécuter une nouvelle déclaration avec un numéro personnalisé de? à chaque fois cependant.
Chris Mazzola

3

Vous pouvez utiliser la méthode setArray comme mentionné dans ce 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
cela n'est pas pris en charge par tous les pilotes, si la fonctionnalité n'est pas prise en charge, vous obtiendrez SQLFeatureNotSupportedException
sans nom

Malheureusement, mon chauffeur ne le prend pas en charge
EdXX

3

Vous pouvez utiliser Collections.nCopiespour générer une collection d'espaces réservés et les rejoindre en utilisant 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
     */
}

Semble être la meilleure solution jusqu'à présent lors de l'utilisation d'Oracle JDBC ...
jansohn

2

Voici une solution complète en Java pour créer l'instruction préparée pour vous:

/*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 permet de transmettre java.util.Lists à NamedParameterJdbcTemplate , qui automatise la génération de (?,?,?, ...,?), En fonction du nombre d'arguments.

Pour Oracle, cette publication de blog traite de l'utilisation de oracle.sql.ARRAY (Connection.createArrayOf ne fonctionne pas avec Oracle). Pour cela, vous devez modifier votre instruction SQL:

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

La fonction de table Oracle transforme le tableau passé en une table similaire à une valeur utilisable dans l' INinstruction.


1

essayez d'utiliser la fonction instr?

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

puis

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

Certes, c'est un peu un hack sale, mais cela réduit les possibilités d'injection SQL. Fonctionne de toute façon dans Oracle.


Oh, et je suis conscient qu'il n'utilisera pas d'index
stjohnroe

cela ne fonctionnerait pas pour certaines chaînes, par exemple, si la chaîne contient un ','.
David Portabella

1

Sormula prend en charge l'opérateur SQL IN en vous permettant de fournir un objet java.util.Collection en tant que paramètre. Il crée une déclaration préparée avec un? pour chacun des éléments de la collection. Voir l' exemple 4 (SQL dans l'exemple est un commentaire pour clarifier ce qui est créé mais n'est pas utilisé par Sormula).


1

à la place d'utiliser

SELECT my_column FROM my_table where search_column IN (?)

utiliser la déclaration SQL comme

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

et

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

ou utiliser une procédure stockée, ce serait la meilleure solution, car les instructions sql seront compilées et stockées dans le serveur DataBase


1

J'ai rencontré un certain nombre de limitations liées à la déclaration préparée:

  1. Les instructions préparées ne sont mises en cache que dans la même session (Postgres), donc cela ne fonctionnera vraiment qu'avec le pool de connexions
  2. Un grand nombre d'instructions préparées différentes telles que proposées par @BalusC peuvent entraîner un débordement du cache et les instructions précédemment mises en cache seront supprimées
  3. La requête doit être optimisée et utiliser des indices. Cela semble évident, cependant, par exemple, la déclaration ANY (ARRAY ...) proposée par @Boris dans l'une des meilleures réponses ne peut pas utiliser d'indices et la requête sera lente malgré la mise en cache
  4. L'instruction préparée met également en cache le plan de requête et les valeurs réelles de tous les paramètres spécifiés dans l'instruction ne sont pas disponibles.

Parmi les solutions proposées, je choisirais celle qui ne diminue pas les performances des requêtes et fait le moins de requêtes. Ce sera le # 4 (regroupant quelques requêtes) du lien @Don ou en spécifiant des valeurs NULL pour les «?» Inutiles marques proposées par @Vladimir Dyuzhev


1

Je viens de trouver une option spécifique à PostgreSQL pour cela. C'est un peu un hack, et il a ses propres avantages, inconvénients et limitations, mais il semble fonctionner et n'est pas limité à un langage de développement, une plate-forme ou un pilote PG spécifique.

L'astuce est bien sûr de trouver un moyen de passer une collection de valeurs de longueur arbitraire en tant que paramètre unique, et de faire en sorte que la base de données la reconnaisse comme plusieurs valeurs. La solution que je travaille est de construire une chaîne délimitée à partir des valeurs de la collection, de passer cette chaîne en tant que paramètre unique et d'utiliser string_to_array () avec le casting requis pour que PostgreSQL l'utilise correctement.

Donc, si vous voulez rechercher "foo", "blah" et "abc", vous pouvez les concaténer ensemble dans une seule chaîne comme: "foo, blah, abc". Voici le SQL simple:

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

Vous changeriez évidemment la distribution explicite en ce que vous vouliez que votre tableau de valeurs résultant soit - int, texte, uuid, etc. Et parce que la fonction prend une seule valeur de chaîne (ou deux je suppose, si vous voulez personnaliser le délimiteur ainsi), vous pouvez le passer comme paramètre dans une instruction préparée:

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

C'est même assez flexible pour supporter des choses comme les comparaisons LIKE:

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

Encore une fois, il ne fait aucun doute que c'est un hack, mais cela fonctionne et vous permet toujours d'utiliser des instructions préparées précompilées qui prennent * ahem * des paramètres discrets , avec les avantages de sécurité et (peut-être) de performance qui l'accompagnent. Est-il conseillé et réellement performant? Naturellement, cela dépend, car vous avez une analyse de chaîne et éventuellement un cast avant que votre requête ne s'exécute. Si vous vous attendez à envoyer trois, cinq, quelques dizaines de valeurs, bien sûr, c'est très bien. Quelques milliers? Ouais, peut-être pas tellement. YMMV, des limitations et exclusions s'appliquent, aucune garantie expresse ou implicite.

Mais ça marche.


0

Juste pour être complet: tant que l'ensemble de valeurs n'est pas trop grand, vous pouvez également simplement construire une instruction de type chaîne comme

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

que vous pourriez ensuite passer à prepare (), puis utiliser setXXX () dans une boucle pour définir toutes les valeurs. Cela semble dégoûtant, mais de nombreux "gros" systèmes commerciaux font régulièrement ce genre de choses jusqu'à ce qu'ils atteignent des limites spécifiques aux bases de données, comme 32 Ko (je pense que c'est le cas) pour les instructions dans Oracle.

Bien sûr, vous devez vous assurer que l'ensemble ne sera jamais déraisonnablement volumineux, ou faire un piégeage d'erreurs dans le cas où il l'est.


Oui tu as raison. Mon objectif dans ce cas était de réutiliser le PreparedStatement avec différents nombres d'éléments à chaque fois.
Chris Mazzola

3
L'utilisation de "OU" masquerait l'intention. Restez avec "IN" car il est plus facile à lire et l'intention est plus claire. La seule raison de passer est que les plans de requête étaient différents.
James Schek

0

Suivant l'idée d'Adam. Faites votre instruction préparée sorte de sélectionner my_column de my_table où search_column dans (#) Créez une chaîne x et remplissez-la avec un certain nombre de "?,?,?" en fonction de votre liste de valeurs Ensuite, changez simplement le # dans la requête pour votre nouvelle chaîne x un peuplement


0

Générez la chaîne de requête dans le PreparedStatement pour avoir un nombre de? Correspondant au nombre d'éléments dans votre liste. Voici un exemple:

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
Il n'est plus nécessaire d'utiliser StringBuilder. Le compilateur convertit les signes + en StringBuilder.append () de toute façon, il n'y a donc aucun impact sur les performances. Essayez-vous :)
neu242

5
@ neu242: Oh oui, le compilateur utilise StringBuilder. Mais pas comme vous le pensez. En décompilant, generateQsForInvous pouvez voir que par itération de boucle, deux nouveaux StringBuildersont alloués et toStringsont appelés sur chacun. L' StringBuilderoptimisation n'attrape que des choses comme, "x" + i+ "y" + jmais ne s'étend pas au-delà d'une expression.
AH

@ neu242 Ne pouvez-vous pas utiliser ps.setObject(1,items)au lieu d'itérer sur la liste puis de définir le paramteres?
Neha Choudhary

0

Il existe différentes approches alternatives que nous pouvons utiliser pour la clause IN dans PreparedStatement.

  1. Utilisation de requêtes uniques - performances les plus lentes et consommatrices de ressources
  2. Utilisation de StoredProcedure - la plus rapide mais spécifique à la base de données
  3. Création d'une requête dynamique pour PreparedStatement - Bonnes performances mais ne bénéficie pas de la mise en cache et PreparedStatement est recompilé à chaque fois.
  4. Utilisez NULL dans les requêtes PreparedStatement - Performances optimales, fonctionne très bien lorsque vous connaissez la limite des arguments de la clause IN. S'il n'y a pas de limite, vous pouvez exécuter des requêtes par lots. Un exemple d'extrait de code est;

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

Vous pouvez vérifier plus de détails sur ces approches alternatives ici .


"Création d'une requête dynamique pour PreparedStatement - Bonnes performances mais ne bénéficie pas de la mise en cache et PreparedStatement est recompilé à chaque fois." la mise en cache et en évitant les recompilations est ce qui rend une instruction préparée performante. Par conséquent, je ne suis pas d'accord avec votre affirmation. Cela empêchera cependant l'injection SQL car vous limitez l'entrée concaténée / dynamique à une virgule.
Brandon

Je suis d'accord avec vous, cependant "bonne performance" est ici pour ce scénario spécifique. C'est plus performant que l'approche 1, mais l'approche 2 est la plus rapide.
Pankaj

0

Dans certaines situations, l'expression rationnelle peut être utile. Voici un exemple que j'ai vérifié sur Oracle, et cela fonctionne.

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

Mais il présente un certain nombre d'inconvénients:

  1. Toute colonne qu'il a appliquée doit être convertie en varchar / char, au moins implicitement.
  2. Besoin d'être prudent avec les caractères spéciaux.
  3. Cela peut ralentir les performances - dans mon cas, la version IN utilise l'analyse d'index et de plage, et la version REGEXP effectue une analyse complète.

0

Après avoir examiné diverses solutions dans différents forums et ne pas avoir trouvé une bonne solution, je pense que le hack ci-dessous que j'ai trouvé est le plus facile à suivre et à coder:

Exemple: supposons que vous ayez plusieurs paramètres à passer dans la clause 'IN'. Il suffit de mettre une chaîne factice à l'intérieur de la clause 'IN', par exemple, "PARAM" indique la liste des paramètres qui viendront à la place de cette chaîne factice.

    select * from TABLE_A where ATTR IN (PARAM);

Vous pouvez collecter tous les paramètres dans une seule variable String dans votre code Java. Cela peut être fait comme suit:

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

Vous pouvez ajouter tous vos paramètres séparés par des virgules dans une seule variable String, 'param1', dans notre cas.

Après avoir collecté tous les paramètres dans une seule chaîne, vous pouvez simplement remplacer le texte factice dans votre requête, c'est-à-dire "PARAM" dans ce cas, par le paramètre String, c'est-à-dire param1. Voici ce que tu dois faire:

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

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

Vous pouvez maintenant exécuter votre requête à l'aide de la méthode executeQuery (). Assurez-vous simplement que le mot "PARAM" ne figure nulle part dans votre requête. Vous pouvez utiliser une combinaison de caractères spéciaux et d'alphabets au lieu du mot "PARAM" afin de vous assurer qu'il n'y a aucune possibilité qu'un tel mot apparaisse dans la requête. J'espère que vous avez la solution.

Remarque: Bien que ce ne soit pas une requête préparée, elle fait le travail que je voulais que mon code fasse.


0

Juste pour être complet et parce que je n'ai vu personne d'autre le suggérer:

Avant de mettre en œuvre l'une des suggestions compliquées ci-dessus, vérifiez si l'injection SQL est effectivement un problème dans votre scénario.

Dans de nombreux cas, la valeur fournie à IN (...) est une liste d'ID qui ont été générés de manière à être sûr qu'aucune injection n'est possible ... (par exemple, les résultats d'un précédent select some_id from some_table où une_condition.)

Si tel est le cas, vous pouvez simplement concaténer cette valeur et ne pas utiliser les services ou l'instruction préparée pour cela ou les utiliser pour d'autres paramètres de cette requête.

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

0

PreparedStatement ne fournit aucun bon moyen de traiter la clause SQL IN. Par http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Vous ne pouvez pas substituer des éléments destinés à faire partie de l'instruction SQL. Cela est nécessaire car si le SQL lui-même peut changer, le le pilote ne peut pas précompiler l'instruction. Il a également pour effet secondaire d'empêcher les attaques par injection SQL. " J'ai fini par utiliser l'approche suivante:

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 est la meilleure solution mais elle n'est pas disponible pour de nombreux pilotes plus anciens. La solution de contournement suivante peut être utilisée dans 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);
}

Cette solution est meilleure que les autres solutions laides tout en boucle où la chaîne de requête est construite par itérations manuelles


0

Cela a fonctionné pour moi (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)";
}

spécifiez la liaison:

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

Mon exemple pour les bases de données SQLite et Oracle.

La première boucle For est destinée à la création d'objets PreparedStatement.

La deuxième boucle For est destinée à la fourniture de valeurs pour les paramètres 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

Ma solution (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 est le tableau qui contient vos entrées / clés / champs, etc.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.