PreparedStatement IN yan tümcesi alternatifleri?


343

SQL enjeksiyon saldırısı güvenlik sorunları nedeniyle birden çok değer için desteklenmeyen, INörnekleriyle bir SQL yan tümcesi kullanmak için en iyi geçici çözümler nelerdir java.sql.PreparedStatement: Bir ?yer tutucu, değerler listesi yerine bir değeri temsil eder.

Aşağıdaki SQL deyimini düşünün:

SELECT my_column FROM my_table where search_column IN (?)

Kullanım preparedStatement.setString( 1, "'A', 'B', 'C'" );, esasen kullanım nedenlerinin çözümünde çalışmayan bir girişimdir ?.

Hangi çözümler mevcut?


1
Oscar, sanırım bir IN yan tümcesine ihtiyacınız varsa (?,?, ....) dinamik nesli en basit çözümdür, ancak özel durumumda performans yeterli olduğu için bireysel çağrılara bıraktım.
Chris Mazzola

6
Hazırlanan ifadelerin avantajlarından biri de verimliliğin verimlilik için bir kez derlenebilmesidir. Bu cümledeki dinamiği dinamik hale getirerek hazırlanan ifadeyi etkili bir şekilde yok eder.

2
Aslında, bu MySQL (parametre değeri olarak bir String dizisi ayarlamak için setObject kullanarak) için çalışır. Hangi DB'yi kullanıyorsunuz?
Frans


Yanıtlar:


194

Mevcut çeşitli seçeneklerin analizi ve her birinin artıları ve eksileri burada mevcuttur .

Önerilen seçenekler:

  • SELECT my_column FROM my_table WHERE search_column = ?Her bir değer için onu hazırlayın , çalıştırın ve sonuçları istemci tarafında BİRLİKTE. Sadece bir tane hazırlanmış ifade gerektirir. Yavaş ve acı verici.
  • Hazırlayın SELECT my_column FROM my_table WHERE search_column IN (?,?,?)ve yürütün. Her liste boyutu için hazırlanmış bir açıklama gerektirir. Hızlı ve açık.
  • Hazırlayın SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...ve yürütün. [Veya UNION ALLbu noktalı virgüllerin yerine kullanın . --ed] Liste boyutu başına hazırlanmış bir ifade gerektirir. Aptalca yavaş, kesinlikle daha kötü WHERE search_column IN (?,?,?), bu yüzden blogcu neden önerdi bilmiyorum.
  • Sonuç kümesini oluşturmak için saklı yordamı kullanın.
  • N farklı liste boyutu sorgusu hazırlayın; diyelim ki, 2, 10 ve 50 değer. 6 farklı değere sahip bir IN listesi aramak için size-10 sorgusunu benzeyecek şekilde yerleştirin SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). İyi bir sunucu, sorguyu çalıştırmadan önce yinelenen değerleri optimize eder.

Bu seçeneklerin hiçbiri süper harika değil.

Bu yerlerde eşit aklı başında alternatiflerle yinelenen sorular cevaplandı, hala hiçbiri süper harika:

JDBC4 ve destekleyen bir sunucu kullanıyorsanız Doğru Yanıt, burada açıklandığı gibi x = ANY(y)kullanmaktır PreparedStatement.setArray:

Yine de setArrayIN listeleriyle çalışmanın bir yolu yok gibi görünüyor .


Bazen SQL deyimleri çalışma zamanında yüklenir (örneğin, bir özellikler dosyasından), ancak değişken sayıda parametre gerektirir. Bu gibi durumlarda, önce sorguyu tanımlayın:

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

Ardından, sorguyu yükleyin. Ardından, çalıştırmadan önce parametre sayısını belirleyin. Parametre sayısı bilindikten sonra, şunu çalıştırın:

sql = any( sql, count );

Örneğin:

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

JDBC 4 belirtimi yoluyla bir diziyi geçirmenin desteklenmediği belirli veritabanları için, bu yöntem yavaşın = ?daha hızlı IN (?)yan tümce durumuna dönüştürülmesini kolaylaştırabilir ve bu yöntem daha sonra anyyöntem çağrılarak genişletilebilir .


123

PostgreSQL için çözüm:

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

veya

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
iyi görünüyor. bu kodun hangi kısmı PostreSQL'e özgüdür? "where search_column = HERHANGİ (?)"? veya connection.createArrayOf? veya başka bir şey?
David Portabella

1
Parçası nedeniyle PostgreSQL'e göre daha JDBC4'e özgü olduğunu düşünüyorum .createArrayOf(), ancak kullanıcının semantiğinin katı semantiklerin ArrayJDBC belirtimi ile tanımlandığından emin değilim .
lvella

3
Eğer .createArrayOfçalışmazsa, dizgi hazır bilgisini kendi manuel olarak oluşturabilirsiniz String arrayLiteral = "{A,\"B \", C,D}" ("B" nin C yokken bir boşluğa sahip olduğuna dikkat edin) ve sonra statement.setString(1,arrayLiteral)hazırlanan deyimin olduğu yer ... IN (SELECT UNNEST(?::VARCHAR[]))veya ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: ANYa ile çalıştığını sanmıyorum SELECT.)
ADTC

Harika bir çözüm! Gerçekten benim için günü kurtardım. Tamsayı dizisi için createArrayOf () 'un ilk parametresinde "int" kullandım ve iyi görünüyor. Bu ilk parametre, belgelere dayanarak DB'ye özgü görünür.
Emmanuel Touzery

2
Bu en temiz çözüm gibi görünüyor. Herkes HSQLDB belirli sözdizimi arıyor: Ben IN (UNNEST (?)) İle çalışmak için başardı
aureianimus

19

Basit bir yol yok AFAIK. Hedef, ifade önbellek oranını yüksek tutmaksa (yani her parametre sayısı başına bir ifade oluşturmamak), aşağıdakileri yapabilirsiniz:

  1. birkaç (örn. 10) parametreli bir ifade oluşturun:

    ... NEREDE (?,?,?,?,?,?,?,?,?,?) ...

  2. Tüm gerçek parametreleri bağlama

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

  3. Gerisini NULL olarak bağla

    setNull (3, Tür.VARCHAR) ... setNull (10, Tür.VARCHAR)

NULL hiçbir şeyle eşleşmez, bu yüzden SQL plan oluşturucu tarafından optimize edilir.

Bir Listeyi DAO işlevine geçirdiğinizde mantığın otomatikleştirilmesi kolaydır:

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

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}

"NULL hiçbir şeyle eşleşmez" - NULLSorguda NULLveritabanındaki bir değer eşleşir mi?
Craig McQueen

5
@CraigMcQueen Hayır olmazdı. ANSI standardına göre null, null ile eşleşmiyor.
Dawood ibn Kareem

IS NULL anahtar sözcüğünü kullanarak NULL ile eşleşebilirsiniz. Birleştirilen tabloda bulunmayan satırları algılamanın güzel bir yolu, IS NULL ile birlikte bir LEFT JOIN kullanmaktır. 'SELECT a.URL, b.URL TABLE_A'dan SOL BİR BİRLEŞTİRME TABLO_B b AÇIK a_A.URL = b_B.URL NEREDE b.URL NULL NEREDİR' Bu, A tablosundaki B tablosunda eşleşmeyen tüm satırları gösterecektir.
Jens Tandstad

3
Buna rağmen dikkatli olun. NOT INve INnull'ları aynı şekilde işlemeyin. Bunu çalıştırın ve neler olduğunu görün: select 'Matched' as did_it_match where 1 not in (5, null); Sonra nullbüyüyü kaldırın ve izleyin.
Brandon

Veya tüm ekstra parametreleri önceki parametrelerin değerine ayarlayabilirsiniz. İyi bir DB motoru bunları filtreleyecektir. Yani a IN (1,2,3,3,3,3,3)aynıdır a IN (1,2,3). Ayrıca NOT INaksine çalışır a NOT IN (1,2,3,null,null,null,null)(her any_value != NULLzaman yanlış olduğu gibi hiçbir satır döndürmez ).
Ruslan Stelmachenko

11

Hoş olmayan bir çözüm, ancak kesinlikle uygulanabilir bir iç içe sorgu kullanmaktır. İçinde bir sütun bulunan geçici bir tablo MYVALUES oluşturun. Değer listenizi MYVALUES tablosuna ekleyin. Sonra yürütün

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

Çirkin, ancak değer listeniz çok büyükse uygun bir alternatif.

Bu teknik, optimize ediciden potansiyel olarak daha iyi sorgu planlarının ek avantajına sahiptir (bir sayfayı birden fazla değer için kontrol edin, tablolar değer başına sadece bir kez yerine, vb.) Veritabanınız hazırlanmış ifadeleri önbelleğe almazsa ek yükten tasarruf edebilir. "INSERTS" cihazınızın toplu olarak yapılması gerekir ve MYVALUES tablonun minimum kilitleme veya diğer yüksek tepeli korumalara sahip olması için ince ayar yapılması gerekebilir.


Bir kerede my_table bir değerini sorgulamanın ne gibi avantajları olur?
Paul Tomblin

3
Sorgu iyileştirici, yüklü bir sayfadan olası tüm eşleşmeleri alarak G / Ç yükünü azaltabilir. Tablo taramaları veya dizin taramaları, değer başına bir kez yerine bir kez gerçekleştirilebilir. Değer ekleme yükü, toplu işlemlerle azaltılabilir ve birkaç sorgudan daha az olabilir.
James Schek

1
iyi görünüyor, ancak eşzamanlılık ile ilgili sorunlar olabilir. jdbc belirtimi bellekte geçici bir anonim tablo oluşturmanın bir yolunu içeriyor mu? ya da böyle bir şey, mümkünse jdbc-satıcıya özgü değil mi?
David Portabella

9

İn () operatörünün sınırlamaları tüm kötülüklerin köküdür.

Önemsiz durumlar için çalışır ve "hazırlanan ifadenin otomatik oluşturulması" ile genişletebilirsiniz, ancak her zaman sınırları vardır.

  • değişken sayıda parametreye sahip bir ifade oluşturuyorsanız, bu, her çağrıda bir sql ayrıştırma yükü oluşturur
  • birçok platformda, in () operatörünün parametre sayısı sınırlıdır
  • tüm platformlarda, toplam SQL metin boyutu sınırlıdır ve parametreler için 2000 yer tutucunun gönderilmesini imkansız hale getirir.
  • JDBC sürücüsü sınırlamalarına sahip olduğundan 1000-10k bağlama değişkenlerini göndermek mümkün değildir

İn () yaklaşımı bazı durumlar için yeterince iyi olabilir, ancak roket kanıtı değil :)

Roket geçirmez çözüm, isteğe bağlı sayıda parametreyi ayrı bir çağrıda (örneğin bir parametre bloğunu geçirerek) geçirmek ve daha sonra bunları SQL'de temsil etmek ve nerede kullanmak için bir görünüme (veya başka bir yol) sahip olmaktır. kriterleri.

Bir kaba kuvvet varyantı burada http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Ancak PL / SQL kullanabiliyorsanız, bu karışıklık oldukça temiz olabilir.

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;

Ardından, parametreye isteğe bağlı olarak virgülle ayrılmış müşteri kimlikleri iletebilirsiniz ve:

  • Seçim için SQL kararlı olduğu için ayrıştırma gecikmesi olmayacak
  • ardışık işlev karmaşıklığı yok - sadece bir sorgu
  • SQL, IN operatörü yerine basit bir birleşim kullanıyor ve bu da oldukça hızlı
  • sonuçta bunun başparmak iyi bir kuraldır değil o, Oracle, MySQL veya benzeri basit bir veritabanı motorları fazlasının teklifler ışık yılı olduğu için, herhangi bir düz seçme veya DML ile veritabanı vurmak. PL / SQL, depolama modelini uygulama etki alanı modelinizden etkili bir şekilde gizlemenizi sağlar.

Buradaki hile:

  • uzun dizeyi kabul eden bir çağrıya ihtiyacımız var ve db oturumunun ona erişebileceği bir yerde depolamamız gerekiyor (örneğin, basit paket değişkeni veya dbms_session.set_context)
  • bunu satırlara ayrıştırabilecek bir görünüme ihtiyacımız var
  • ve sonra sorguladığınız kimlikleri içeren bir görünümünüz var, bu yüzden tek yapmanız gereken sorgulanan tabloya basit bir birleşim.

Görünüm şöyle:

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

burada aux_in_list.getpayload orijinal giriş dizesini ifade eder.


Olası bir yaklaşım pl / sql dizilerini (yalnızca Oracle tarafından desteklenir) geçirmektir, ancak saf SQL'de bunları kullanamazsınız, bu nedenle her zaman bir dönüşüm adımı gereklidir. Dönüşüm SQL'de yapılamaz, bu nedenle sonuçta, tüm parametrelerle bir clob'u dizede geçirmek ve bir görünüme dönüştürmek en etkili çözümdür.


6

İşte kendi uygulamamda bunu nasıl çözdüm. İdeal olarak, Dizeler için + kullanmak yerine bir StringBuilder kullanmanız gerekir.

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

Somut sayılar yerine yukarıdaki x gibi bir değişken kullanmak, sorguyu daha sonra değiştirmeye karar verirseniz çok yardımcı olur.


5

Hiç denemedim, ama .setArray () aradığınızı yapar mı?

Güncelleme : Açıkçası hayır. setArray, yalnızca önceki bir sorgudan aldığınız bir ARRAY sütunundan veya ARRAY sütunu olan bir alt sorgudan gelen bir java.sql.Array ile çalışıyor gibi görünüyor.


4
Tüm veritabanlarıyla çalışmaz, ancak "doğru" yaklaşımdır.
skaffman

Tüm sürücüler demek istiyorsun. Bazı sürücüler bu yıl (geçen yüzyıl?) Standardının tescilli eşdeğerlerine sahiptir. Başka bir yol, bir değer
kümesini

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Sun'a göre, Dizi içeriği [genellikle] sunucu tarafında kalır ve gerektiğinde çekilir. PreparedStatement.setArray (), istemci tarafında yeni bir Array oluşturamaz, önceki ResultSet'ten bir Array geri gönderebilir.
Chris Mazzola

5

Geçici çözümüm:

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

Artık bir tablodaki bazı değerleri elde etmek için bir değişken kullanabilirsiniz:

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

Yani, hazırlanan ifade şöyle olabilir:

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

Saygılarımızla,

Javier Ibanez


Bu PL / SQL, evet. Diğer veritabanlarında çalışmaz. Bu uygulamanın, giriş parametrelerinin bir sınırlaması olduğunu unutmayın - toplam uzunluk 32k karakterle sınırlıdır - ve boru hattı işlevine çağrı Oracle'ın PL / SQL ve SQL motorları arasında bir bağlam değişikliği yaptığı için bir performans sınırlaması vardır.
Gee Bee

3

Sana sorgu dizesi (temel dize kullanımı kullanarak) oluşturmak herhalde PreparedStatementbir takım olması ?'ler Listenizdeki öğelerin sayısını eşleşen.

Tabii ki ORsorgunuzda zincirlenmiş bir dev oluşturmaktan sadece bir adım uzaktasınız , ancak ?sorgu dizesinde doğru sayıya sahip olmadan , bu konuda başka nasıl çalışabileceğinizi göremiyorum.


Farklı bir şekilde göndermek istediğim için benim için gerçekten bir çözüm değil mi? ps'yi her aradığımda. Ama düşünmediğimi sanma. : P
Chris Mazzola

4
Başka bir saldırı: çok sayıda parametre yer tutucusu kullanabilirsiniz - sahip olduğunuz en uzun değer listesi kadar - ve değer listeniz daha kısasa, değerleri tekrarlayabilirsiniz: ... NEREDEN arama alanı IN (? ,?,?,?,?,?,?,?) ve ardından değerleri sağlayın: A, B, C, D, A, B, C, D
Bill Karwin

1
Ama genel olarak Adam'ın çözümünü destekliyorum: SQL'i dinamik olarak oluşturun ve birleştirin? geçmeniz gereken değerlerin sayısını eşleştirmek için yer tutucular.
Bill Karwin

Bill, PreparedStatement'ı tekrar kullanmak istemezsem bu çözüm uygulanabilir. Başka bir çözüm, tek param çağrısını birden çok kez yapmak ve sonuçları istemci tarafında biriktirmektir. Muhtemelen? Numaralı özel sayı ile yeni bir Bildirim oluşturmak / yürütmek daha verimli olur. her seferinde.
Chris Mazzola

3

Sen belirtildiği gibi setArray yöntemini kullanabilirsiniz bu 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
bu özellik tüm sürücüler tarafından desteklenmez, özellik desteklenmiyorsa SQLFeatureNotSupportedException
adsız

Ne yazık ki
sürücüm

3

Bir Collections.nCopiesyer tutucu koleksiyonu oluşturmak ve aşağıdakileri kullanarak bunlara katılmak için kullanabilirsiniz 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
     */
}

Oracle JDBC kullanırken şimdiye kadarki en iyi çözüm gibi görünüyor ...
jansohn

2

Java'nın sizin için hazırlanan ifadeyi oluşturması için eksiksiz bir çözüm:

/*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 , argümanların sayısına uygun olarak (?,?,?, ...,?) Üretimini otomatik hale getiren NamedParameterJdbcTemplate'e java.util.Lists iletilmesini sağlar .

Oracle için bu blog gönderisi oracle.sql.ARRAY (Connection.createArrayOf Oracle ile çalışmaz) kullanımını tartışır. Bunun için SQL ifadenizi değiştirmeniz gerekir:

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

Oracle tablo işlevi değeri kullanılabilir gibi bir tabloya geçirilen bir dizi dönüştüren INtablosu.


1

instr işlevini kullanmayı deneyin?

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

sonra

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

Kuşkusuz bu biraz kirli bir kesmek, ama sql enjeksiyon fırsatlarını azaltmıyor. Yine de kehanette çalışıyor.


Oh, ve ben endeksleri kullanmayacağım farkındayım
stjohnroe

bazı dize için çalışmaz, örneğin dize ',' içeriyorsa.
David Portabella

1

Sormula , parametre olarak bir java.util.Collection nesnesi sağlamanıza izin vererek SQL IN işlecini destekler. ? İle hazırlanmış bir ifade oluşturur. elemanların her biri için koleksiyon. Örnek 4'e bakın (örnekte SQL, oluşturulan ve Sormula tarafından kullanılmayanları açıklığa kavuşturmak için bir açıklamadır).


1

kullanmak yerine

SELECT my_column FROM my_table where search_column IN (?)

SQL deyimini şu şekilde kullanın:

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

ve

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

veya sql deyimleri derlenip DataBase sunucusunda depolanacağı için saklı yordam kullanın, bu en iyi çözüm olacaktır


1

Hazırlanan ifadeyle ilgili bir takım sınırlamalarla karşılaştım:

  1. Hazırlanan ifadeler yalnızca aynı oturumda (Postgres) önbelleğe alınır, bu nedenle gerçekten yalnızca bağlantı havuzu oluşturma ile çalışır
  2. @BalusC tarafından önerilen birçok farklı hazırlanmış ifade, önbelleğin aşırı dolmasına neden olabilir ve önceden önbelleğe alınmış ifadeler bırakılır
  3. Sorgu optimize edilmeli ve dizinler kullanılmalıdır. Ancak kulağa çok açık geliyor, örneğin @Boris tarafından en iyi yanıtlardan birinde önerilen HERHANGİ BİR (ARRAY ...) ifadesi dizinleri kullanamıyor ve önbelleklemeye rağmen sorgu yavaş olacak
  4. Hazırlanan deyim de sorgu planını önbelleğe alır ve deyimde belirtilen parametrelerin gerçek değerleri kullanılamaz.

Önerilen çözümler arasında sorgu performansını azaltmayan ve daha az sorgu yapan bir çözüm seçtim. Bu @Don bağlantısındaki # 4 (birkaç sorguyu topluyor) veya gereksiz '?' @Vladimir Dyuzhev tarafından önerilen işaretler


1

Bunun için PostgreSQL'e özel bir seçenek geliştirdim. Biraz hack ve kendi artıları, eksileri ve sınırlamaları ile birlikte geliyor, ancak işe yarıyor gibi görünüyor ve belirli bir geliştirme dili, platform veya PG sürücüsü ile sınırlı değil.

İşin püf noktası, keyfi bir uzunluk değerlerini tek bir parametre olarak geçirmenin bir yolunu bulmak ve db'nin birden çok değer olarak tanımasını sağlamaktır. Çalıştığım çözüm, koleksiyondaki değerlerden sınırlandırılmış bir dize oluşturmak, bu dizeyi tek bir parametre olarak iletmek ve PostgreSQL için düzgün bir şekilde kullanmak için gerekli döküm ile string_to_array () kullanmaktır.

Dolayısıyla, "foo", "blah" ve "abc" kelimelerini aramak istiyorsanız, bunları "foo, blah, abc" gibi tek bir dizede birleştirebilirsiniz. İşte düz SQL:

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

Açıkça ortaya çıkan değer dizinizin olmasını istediğiniz her şeye değiştirirsiniz - int, metin, uuid, vb. Ve işlev tek bir dize değeri alıyorsa (veya ayırıcıyı özelleştirmek istiyorsanız iki ayrıca) hazırlanmış bir ifadede parametre olarak iletebilirsiniz:

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

Bu, LIKE karşılaştırmaları gibi şeyleri destekleyecek kadar esnektir:

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

Yine, hiçbir soru bu bir hack, ama işe yarıyor ve beraberinde gelen güvenlik ve (belki) performans faydaları ile * ahem * ayrık parametreler alan önceden derlenmiş hazırlanmış ifadeler kullanmanızı sağlar. Tavsiye edilir ve gerçekte performans gösterir mi? Doğal olarak, sorgunuz çalışmaya başlamadan önce dize ayrıştırma ve döküm işlemi yaptığınıza göre değişir. Üç, beş, birkaç düzine değer göndermeyi bekliyorsanız, muhtemelen iyi olabilir. Birkaç bin mi? Evet, belki o kadar da değil. YMMV, sınırlamalar ve istisnalar geçerlidir, hiçbir açık veya zımni garanti verilmez.

Ama işe yarıyor.


0

Sadece şeyiyle: Çok uzun değerler kümesi çok büyük değil, sen olabilir ayrıca basitçe gibi bir ifade dize oluşturmak

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

daha sonra ready () öğesine iletebilir ve sonra tüm değerleri ayarlamak için bir döngüde setXXX () yöntemini kullanabilirsiniz. Bu şanslı görünüyor, ancak birçok "büyük" ticari sistem, Oracle'daki ifadeler için 32 KB (sanırım öyle) gibi DB'ye özgü sınırlara ulaşana kadar rutin olarak bu tür bir şey yapıyor.

Tabii ki setin asla makul olmayan büyüklükte olmamasını ya da olması durumunda hata yakalamayı önlemeniz gerekir.


Evet haklısın. Bu durumda amacım PreparedStatement'ı her seferinde farklı sayıda öğe ile yeniden kullanmaktı.
Chris Mazzola

3
"VEYA" kullanmak niyeti gizler. Okunması daha kolay ve amacı daha net olduğu için "IN" ile yapıştırın. Değiştirmenin tek nedeni sorgu planlarının farklı olması.
James Schek

0

Adem'in fikrini takiben. Hazırladığınız ifadeyi my_table'dan my_column öğesini seçin. Burada search_column, (#) Bir String x oluşturun ve bir dizi "?,?,?" İle doldurun. Değerler listenize bağlı olarak, yeni String x an nüfusunuzun sorgusundaki # değerini değiştirmeniz yeterlidir.


0

Listenizdeki öğelerin sayısıyla eşleşen bir kaç? İfadesi olması için PreparedStatement'ta sorgu dizesi oluşturun. İşte bir örnek:

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
Artık StringBuilder kullanmaya gerek yok. Derleyici yine de + işaretlerini StringBuilder.append () öğesine dönüştürür, dolayısıyla performans isabeti yoktur. Kendiniz deneyin :)
neu242

5
@ neu242: Oh evet, derleyici kullanıyor StringBuilder. Ama düşündüğün gibi değil. Ayrıştırma generateQsForIn, döngü yinelemesi başına iki yeni StringBuildertahsis edildiğini ve toStringher birinde çağrıldığını görebilirsiniz. StringBuilderOptimizasyon yalnızca gibi şeyler yakalar "x" + i+ "y" + jama bir ifadesi ötesine geçmez.
AH

@ neu242 ps.setObject(1,items)Listeyi yinelemek ve sonra ayarı yapmak yerine kullanamaz paramteresmısınız?
Neha Choudhary

0

PreparedStatement'ta IN deyimi için kullanabileceğimiz farklı alternatif yaklaşımlar vardır.

  1. Tek Sorguları Kullanma - en yavaş performans ve kaynak yoğun
  2. StoredProcedure Kullanımı - En hızlı ancak veritabanına özgü
  3. PreparedStatement için dinamik sorgu oluşturma - İyi Performans ancak önbelleğe alma avantajından yararlanamaz ve PreparedStatement her seferinde yeniden derlenir.
  4. PreparedStatement sorgularında NULL kullanın - En iyi performans, IN yan tümcesi bağımsız değişkenlerinin sınırını bildiğinizde harika çalışır. Sınır yoksa, sorguları toplu olarak yürütebilirsiniz. Örnek kod snippet'i;

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

Bu alternatif yaklaşımlar hakkında daha fazla bilgiyi burada bulabilirsiniz .


"PreparedStatement için dinamik sorgu oluşturma - İyi Performans ancak önbelleğe alma avantajından yararlanamaz ve PreparedStatement her seferinde yeniden derlenir." önbelleğe almak ve yeniden derlemekten kaçınmak, hazırlanmış bir ifadenin iyi performans göstermesini sağlar. Bu nedenle, iddianıza katılmıyorum. Ancak bu, birleştirilmiş / dinamik girdiyi virgülle sınırladığınız için SQL enjeksiyonunu önleyecektir.
Brandon,

Size katılıyorum, ancak burada "İyi Performans" bu özel senaryo için. Yaklaşım 1'den daha iyi performans gösterir, ancak yaklaşım 2 en hızlıdır.
Pankaj

0

Bazı durumlarda normal ifade yardımcı olabilir. İşte Oracle üzerinde kontrol ettiğim bir örnek ve işe yarıyor.

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

Ancak bununla ilgili bir takım dezavantajlar var:

  1. Uyguladığı herhangi bir sütun, en azından örtük olarak varchar / char'a dönüştürülmelidir.
  2. Özel karakterlere dikkat etmelisin.
  3. Performansı yavaşlatabilir - benim durumumda IN sürümü dizin ve aralık taraması kullanır ve REGEXP sürümü tam tarama yapar.

0

Farklı forumlarda çeşitli çözümleri inceledikten ve iyi bir çözüm bulamadıktan sonra, geldiğim aşağıdaki hack'i takip etmek ve kodlamak en kolay olanı hissediyorum:

Örnek: 'IN' yantümcesinde iletilecek birden çok parametreniz olduğunu varsayalım. "IN" yan tümcesinin içine bir kukla String koyun, diyelim ki, "PARAM" bu kukla String yerine gelecek olan parametrelerin listesini gösterir.

    select * from TABLE_A where ATTR IN (PARAM);

Tüm parametreleri Java kodunuzda tek bir String değişkeninde toplayabilirsiniz. Bu şöyle yapılabilir:

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

Virgüllerle ayrılmış tüm parametrelerin bizim durumumuzda tek bir String değişkeni olan 'param1' içine ekleyebilirsiniz.

Tüm parametreleri tek bir String'e topladıktan sonra, sorgunuzdaki kukla metni, bu durumda "PARAM" yerine String, yani param1 parametresi ile değiştirebilirsiniz. Yapman gerekenler işte burada:

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

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

Şimdi sorgunuzu executeQuery () yöntemini kullanarak yürütebilirsiniz. Sorgunuzda hiçbir yerde "PARAM" kelimesinin bulunmadığından emin olun. Sorguya böyle bir kelimenin gelme olasılığının olmadığından emin olmak için "PARAM" kelimesi yerine özel karakterlerin ve alfabelerin bir kombinasyonunu kullanabilirsiniz. Umarım çözümü bulmuşsundur.

Not: Bu hazırlanmış bir sorgu olmasa da, kodumun yapmasını istediğim işi yapıyor.


0

Sadece bütünlük için ve başka kimsenin önerdiğini görmediğim için:

Yukarıdaki karmaşık önerilerden herhangi birini uygulamadan önce, SQL enjeksiyonunun sizin senaryoda gerçekten bir sorun olup olmadığını düşünün.

Birçok durumda, IN (...) 'a sağlanan değer, hiçbir enjeksiyonun mümkün olmadığından emin olabileceğiniz şekilde oluşturulmuş kimliklerin bir listesidir ... some_condition.)

Bu durumda, bu değeri birleştirebilir ve hizmetleri veya hazırlanmış ifadeyi kullanamaz veya bu sorgunun diğer parametreleri için kullanamazsınız.

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

0

PreparedStatement, SQL IN yan tümcesi ile başa çıkmak için iyi bir yol sağlamaz. Başına http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Sen SQL ifadesinin bir parçası haline getirilmek isteniyor şeyleri yerini alamaz. SQL kendisi değiştirebilir çünkü eğer bu gereklidir, sürücü ifadeyi önceden derleyemiyor. SQL enjeksiyon saldırılarını önlemenin de güzel bir yan etkisi var. " Sonunda şu yaklaşımı kullandım:

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 en iyi çözümdür ancak daha eski sürücüler için mevcut değildir. Java8'de aşağıdaki geçici çözüm kullanılabilir

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

Bu çözüm, sorgu dizesinin manuel yinelemelerle oluşturulduğu döngü çözümlerinden diğer çirkin çözümlerden daha iyidir


0

Bu benim için çalıştı (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)";
}

bağlama belirtin:

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

SQLite ve Oracle veritabanlarına örnek.

İlk For döngüsü bir PreparedStatement Nesnesi Oluşturma içindir.

İkinci For döngüsü PreparedStatement Parametreleri için Değer Sağlamak içindir.

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

Geçici çözümüm (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 girdi / anahtarlarınızı / alanlarınızı vb. içeren dizidir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.