Alternatif klausa PreparedStatement IN?


343

Apa solusi terbaik untuk menggunakan INklausa SQL dengan contoh java.sql.PreparedStatement, yang tidak didukung untuk beberapa nilai karena masalah keamanan serangan injeksi SQL: Satu ?placeholder mewakili satu nilai, bukan daftar nilai.

Pertimbangkan pernyataan SQL berikut:

SELECT my_column FROM my_table where search_column IN (?)

Penggunaan preparedStatement.setString( 1, "'A', 'B', 'C'" );pada dasarnya adalah upaya yang tidak bekerja untuk mencari tahu alasan penggunaannya ?.

Solusi apa yang tersedia?


1
Oscar, saya pikir generasi dinamis (?,?, ....) adalah solusi paling sederhana jika Anda membutuhkan klausa IN, tetapi saya menyerahkannya pada panggilan individual karena kinerjanya cukup dalam kasus spesifik saya.
Chris Mazzola

6
Salah satu keuntungan dari pernyataan yang disiapkan adalah bahwa sohuld dapat dikompilasi satu kali untuk efisiensi. Dengan membuat klausa yang dinamis, ini secara efektif meniadakan pernyataan yang disiapkan.

2
Sebenarnya, ini berfungsi untuk MySQL (menggunakan setObject untuk mengatur larik String sebagai nilai parameter). DB apa yang Anda gunakan?
Frans


Berikut pertanyaan terkait: stackoverflow.com/q/6956025/521799
Lukas Eder

Jawaban:


194

Analisis berbagai opsi yang tersedia, dan pro dan kontra dari masing-masing tersedia di sini .

Opsi yang disarankan adalah:

  • Mempersiapkan SELECT my_column FROM my_table WHERE search_column = ?, melaksanakannya untuk setiap nilai dan UNION hasil sisi klien. Hanya membutuhkan satu pernyataan yang disiapkan. Lambat dan menyakitkan.
  • Mempersiapkan SELECT my_column FROM my_table WHERE search_column IN (?,?,?)dan melaksanakannya. Membutuhkan satu pernyataan yang disiapkan per ukuran-dalam-daftar. Cepat dan jelas.
  • Mempersiapkan SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...dan melaksanakannya. [Atau gunakan UNION ALLsebagai pengganti titik koma itu. --ed] Membutuhkan satu pernyataan yang disiapkan per ukuran-dalam-daftar. Sangat lambat, benar-benar lebih buruk daripada WHERE search_column IN (?,?,?), jadi saya tidak tahu mengapa blogger bahkan menyarankannya.
  • Gunakan prosedur tersimpan untuk membangun set hasil.
  • Mempersiapkan N berbagai ukuran daftar pertanyaan; katakanlah, dengan 2, 10, dan 50 nilai. Untuk mencari IN-list dengan 6 nilai yang berbeda, isi kueri ukuran-10 sehingga terlihat seperti SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Server mana pun yang layak akan mengoptimalkan nilai duplikat sebelum menjalankan kueri.

Namun, tidak satu pun dari opsi ini yang sangat bagus.

Pertanyaan duplikat telah dijawab di tempat-tempat ini dengan alternatif yang sama-sama waras, masih tidak ada yang super hebat:

Jawaban yang Benar, jika Anda menggunakan JDBC4 dan server yang mendukung x = ANY(y), adalah untuk digunakan PreparedStatement.setArrayseperti yang dijelaskan di sini:

Sepertinya tidak ada cara untuk membuat setArraypekerjaan dengan IN-list.


Kadang-kadang pernyataan SQL dimuat saat runtime (misalnya, dari file properti) tetapi memerlukan sejumlah variabel parameter. Dalam kasus seperti itu, tentukan terlebih dahulu kueri:

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

Selanjutnya, muat kueri. Kemudian tentukan jumlah parameter sebelum menjalankannya. Setelah jumlah parameter diketahui, jalankan:

sql = any( sql, count );

Sebagai contoh:

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

Untuk database tertentu di mana melewatkan array melalui spesifikasi JDBC 4 tidak didukung, metode ini dapat memfasilitasi mengubah lambat = ?menjadi IN (?)kondisi klausa yang lebih cepat , yang kemudian dapat diperluas dengan memanggil anymetode.


123

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

atau

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
kelihatan bagus. bagian mana dari kode ini yang spesifik untuk PostreSQL? "where search_column = ANY (?)"? atau koneksi.createArrayOf? atau sesuatu yang lain?
David Portabella

1
Saya pikir ini lebih spesifik-JDBC4 daripada PostgreSQL-spesifik, karena .createArrayOf()bagian itu, tetapi saya tidak yakin semantik ketat untuk pengguna Arrayditentukan oleh spesifikasi JDBC.
lvella

3
Jika .createArrayOftidak berhasil, Anda dapat melakukan pembuatan manual sendiri seperti array literal String arrayLiteral = "{A,\"B \", C,D}" (perhatikan bahwa "B" memiliki spasi sementara C tidak) dan kemudian di statement.setString(1,arrayLiteral)mana pernyataan yang disiapkan adalah ... IN (SELECT UNNEST(?::VARCHAR[]))atau ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: Saya kira tidak ANYbekerja dengan SELECT.)
ADTC

Solusi hebat! Benar-benar menyelamatkan hari untukku. Untuk array integer saya menggunakan "int" pada parameter pertama createArrayOf () dan terlihat bagus. Parameter pertama itu muncul khusus DB, berdasarkan pada dokumentasi.
Emmanuel Touzery

2
Ini sepertinya solusi terbersih. Jika ada yang mencari sintaks khusus HSQLDB: Saya berhasil mendapatkan ini untuk bekerja dengan IN (UNNEST (?))
aureianimus

19

Tidak ada cara sederhana AFAIK. Jika target adalah untuk menjaga rasio cache pernyataan tetap tinggi (yaitu untuk tidak membuat pernyataan per setiap jumlah parameter), Anda dapat melakukan hal berikut:

  1. buat pernyataan dengan beberapa (misalnya 10) parameter:

    ... DI MANA SAJA (?,?,?,?,?,?,?,?,?,?) ...

  2. Bind semua parameter aktual

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

  3. Bind sisanya sebagai NULL

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

NULL tidak pernah cocok dengan apa pun, sehingga dioptimalkan oleh pembuat paket SQL.

Logikanya mudah untuk diotomatisasi ketika Anda memasukkan Daftar ke fungsi 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 tidak pernah cocok dengan apa pun" - Apakah NULLdalam kueri cocok dengan NULLnilai dalam database?
Craig McQueen

5
@CraigMcQueen Tidak, tidak. Null bahkan tidak cocok dengan nol, sesuai dengan standar ANSI.
Dawood ibn Kareem

Anda dapat mencocokkan NULL dengan menggunakan kata kunci IS NULL. Cara yang bagus untuk mendeteksi baris yang tidak ada di tabel bergabung adalah dengan menggunakan LEFT JOIN bersama dengan IS NULL. 'PILIH a.URL, b.URL DARI TABLE_A KIRI BERGABUNG TABLE_B b PADA a_A.URL = b_B.URL DI MANA b.URL ADALAH NULL' Ini akan menampilkan semua baris dalam tabel A yang tidak memiliki kecocokan dalam tabel B.
Jens Tandstad

3
Hati-hati dengan ini. NOT INdan INtidak menangani nulls dengan cara yang sama. Jalankan ini dan lihat apa yang terjadi: select 'Matched' as did_it_match where 1 not in (5, null); Kemudian lepaskan nulldan tonton keajaibannya.
Brandon

Atau Anda dapat mengatur semua params tambahan ke nilai param apa pun sebelumnya. Setiap mesin DB yang layak akan menyaringnya. Begitu a IN (1,2,3,3,3,3,3)juga dengan a IN (1,2,3). Ini juga berfungsi dengan NOT INtidak seperti a NOT IN (1,2,3,null,null,null,null)(yang selalu tidak mengembalikan baris seperti any_value != NULLyang selalu salah).
Ruslan Stelmachenko

11

Solusi yang tidak menyenangkan, tetapi tentu layak dilakukan dengan menggunakan kueri bersarang. Buat tabel sementara MYVALUES dengan kolom di dalamnya. Masukkan daftar nilai Anda ke dalam tabel MYVALUES. Kemudian jalankan

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

Jelek, tetapi merupakan alternatif yang layak jika daftar nilai Anda sangat besar.

Teknik ini memiliki keuntungan tambahan dari rencana kueri yang berpotensi lebih baik dari optimizer (periksa halaman untuk beberapa nilai, tablescan hanya sekali sebagai gantinya sekali per nilai, dll) dapat menghemat overhead jika database Anda tidak men-cache pernyataan yang disiapkan. "INSERTS" Anda harus dilakukan dalam batch dan tabel MYVALUES mungkin perlu diubah agar memiliki penguncian minimal atau perlindungan overhead tinggi lainnya.


Keuntungan apa yang dimiliki jika saya tidak menanyakan satu nilai my_table pada satu waktu?
Paul Tomblin

3
Pengoptimal kueri dapat mengurangi muatan I / O dengan mengambil semua kemungkinan kecocokan dari halaman yang dimuat. Tablescan atau pemindaian indeks dapat dilakukan satu kali, bukan satu kali per nilai. Overhead untuk memasukkan nilai dapat dikurangi dengan operasi batch dan mungkin kurang dari beberapa kueri.
James Schek

1
itu terlihat bagus, tetapi mungkin ada masalah dengan konkurensi. apakah spesifikasi jdbc mengandung cara untuk membuat tabel anonim temporal dalam memori? atau sesuatu seperti itu, jika mungkin tidak jdbc-vendor spesifik?
David Portabella

9

Keterbatasan operator in () adalah akar dari semua kejahatan.

Ini bekerja untuk kasus-kasus sepele, dan Anda dapat memperpanjangnya dengan "generasi otomatis dari pernyataan yang disiapkan" namun selalu ada batasnya.

  • jika Anda membuat pernyataan dengan jumlah variabel variabel, itu akan membuat overhead parse sql di setiap panggilan
  • pada banyak platform, jumlah parameter dalam () operator terbatas
  • pada semua platform, total ukuran teks SQL terbatas, membuat tidak mungkin untuk mengirim 2000 placeholder untuk param di
  • mengirim variabel bind 1000-10k tidak mungkin, karena driver JDBC memiliki keterbatasan

Pendekatan in () bisa cukup baik untuk beberapa kasus, tetapi bukan bukti roket :)

Solusi roket-bukti adalah untuk melewati jumlah parameter yang sewenang-wenang dalam panggilan terpisah (misalnya, dengan mengirimkan sekelompok params), dan kemudian memiliki pandangan (atau cara lain) untuk mewakili mereka dalam SQL dan digunakan di mana Anda kriteria.

Varian brute-force ada di sini http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Namun jika Anda bisa menggunakan PL / SQL, kekacauan ini bisa menjadi sangat rapi.

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;

Kemudian, Anda dapat melewati jumlah id pelanggan yang dipisahkan koma di parameter, dan:

  • tidak akan mendapatkan penundaan parse, karena SQL untuk pilih stabil
  • tidak ada kompleksitas fungsi pipelined - itu hanya satu permintaan
  • SQL menggunakan gabungan sederhana, bukan operator IN, yang cukup cepat
  • setelah semua, itu adalah aturan praktis yang baik untuk tidak mengenai database dengan pilihan biasa atau DML, karena itu adalah Oracle, yang menawarkan tahun cahaya lebih dari MySQL atau mesin database sederhana serupa. PL / SQL memungkinkan Anda untuk menyembunyikan model penyimpanan dari model domain aplikasi Anda secara efektif.

Kuncinya di sini adalah:

  • kita membutuhkan panggilan yang menerima string panjang, dan menyimpan di suatu tempat di mana sesi db dapat mengaksesnya (mis. variabel paket sederhana, atau dbms_session.set_context)
  • maka kita membutuhkan tampilan yang dapat menguraikan ini ke baris
  • dan kemudian Anda memiliki tampilan yang berisi id yang Anda query, jadi semua yang Anda butuhkan adalah gabung sederhana ke tabel yang ditanyakan.

Tampilannya seperti:

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

di mana aux_in_list.getpayload merujuk ke string input asli.


Sebuah pendekatan yang mungkin adalah dengan melewatkan array pl / sql (hanya didukung oleh Oracle), namun Anda tidak dapat menggunakannya dalam SQL murni, oleh karena itu langkah konversi selalu diperlukan. Konversi tidak dapat dilakukan dalam SQL, jadi setelah semua, melewati sebuah clob dengan semua parameter dalam string dan mengubahnya dalam tampilan adalah solusi yang paling efisien.


6

Inilah cara saya menyelesaikannya di aplikasi saya sendiri. Idealnya, Anda harus menggunakan StringBuilder alih-alih menggunakan + untuk 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();
    }

Menggunakan variabel seperti x di atas sebagai ganti angka nyata sangat membantu jika Anda memutuskan untuk mengubah kueri di lain waktu.


mencoba yang sama, tidak bekerja untuk Oracle
Santosh Raviteja

5

Saya belum pernah mencobanya, tetapi apakah .setArray () akan melakukan apa yang Anda cari?

Pembaruan : Jelas tidak. setArray tampaknya hanya berfungsi dengan java.sql.Array yang berasal dari kolom ARRAY yang telah Anda ambil dari kueri sebelumnya, atau subquery dengan kolom ARRAY.


4
Tidak bekerja dengan semua basis data, tetapi ini adalah pendekatan yang "benar".
skaffman

Maksud Anda semua driver. Beberapa driver memiliki hak milik yang setara dengan standar tahun ini (abad lalu?). Cara lain adalah dengan memasukkan sekumpulan nilai ke dalam tabel sementara, tetapi tidak semua database mendukung itu ...
Tom Hawtin - tackline

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/... Menurut Sun, konten Array [biasanya] tetap berada di sisi server dan ditarik sesuai kebutuhan. PreparedStatement.setArray () dapat mengirim kembali Array dari ResultSet sebelumnya, bukan membuat Array baru di sisi klien.
Chris Mazzola

5

Solusi saya adalah:

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

Sekarang Anda bisa menggunakan satu variabel untuk mendapatkan beberapa nilai dalam tabel:

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

Jadi, pernyataan yang disiapkan bisa berupa:

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

Salam,

Javier Ibanez


Ini PL / SQL, ya. Itu tidak akan bekerja di database lain. Perhatikan bahwa implementasi ini memiliki batasan input params - total panjang dibatasi hingga 32rb karakter -, serta pembatasan kinerja karena panggilan ke fungsi pipelined membuat perubahan konteks antara PL / SQL dan mesin SQL dari Oracle.
Wah Wah

3

Saya kira Anda bisa (menggunakan manipulasi string dasar) menghasilkan string kueri PreparedStatementuntuk memiliki sejumlah yang ?cocok dengan jumlah item dalam daftar Anda.

Tentu saja jika Anda melakukan itu Anda hanya selangkah lagi dari menghasilkan raksasa dirantai ORdalam permintaan Anda, tetapi tanpa memiliki jumlah yang tepat ?dalam string kueri, saya tidak melihat bagaimana lagi Anda bisa mengatasi ini.


Bukan solusi bagi saya karena saya ingin mengirim dalam jumlah yang berbeda? setiap kali saya memanggil ps. Tapi jangan berpikir aku tidak mempertimbangkannya. : P
Chris Mazzola

4
Peretasan lain: Anda dapat menggunakan sejumlah besar penampung parameter - sebanyak daftar nilai terpanjang yang akan Anda miliki - dan jika daftar nilai Anda lebih pendek, Anda dapat mengulangi nilai-nilai: ... DI MANA searchfield IN (? ,?,?,?,?,?,?,?) dan kemudian memberikan nilai: A, B, C, D, A, B, C, D
Bill Karwin

1
Tapi secara keseluruhan saya menyukai solusi Adam: menghasilkan SQL secara dinamis, dan menyatukan? placeholder untuk mencocokkan jumlah nilai yang harus Anda lewati.
Bill Karwin

Bill, solusi itu bisa diterapkan jika saya tidak ingin menggunakan kembali PreparedStatement. Solusi lain adalah membuat panggilan param tunggal beberapa kali dan mengumpulkan hasil di sisi klien. Mungkin akan lebih efisien untuk membangun / menjalankan Pernyataan baru dengan nomor khusus? meskipun setiap kali.
Chris Mazzola

3

Anda bisa menggunakan metode setArray seperti yang disebutkan di javadoc ini :

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
ini tidak didukung oleh semua driver, jika fitur ini tidak didukung Anda akan mendapatkan SQLFeatureNotSupportedException
tidak disebutkan namanya

Sayangnya driver saya tidak mendukungnya
EdXX

Ini tidak berfungsi untuk Oracle
Santosh Raviteja

3

Anda dapat menggunakan Collections.nCopiesuntuk menghasilkan koleksi placeholder dan bergabung dengan mereka menggunakan 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
     */
}

Tampaknya menjadi solusi terbaik sejauh ini ketika menggunakan Oracle JDBC ...
jansohn

2

Berikut solusi lengkap di Jawa untuk membuat pernyataan yang disiapkan untuk Anda:

/*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 memungkinkan lewat java.util.Lists ke NamedParameterJdbcTemplate , yang mengotomatiskan generasi (?,?,?, ...,?), Yang sesuai untuk jumlah argumen.

Untuk Oracle, posting blog ini membahas penggunaan oracle.sql.ARRAY (Connection.createArrayOf tidak berfungsi dengan Oracle). Untuk ini, Anda harus mengubah pernyataan SQL Anda:

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

The tabel fungsi oracle mengubah array berlalu ke dalam tabel seperti nilai yang dapat digunakan dalam INpernyataan.


1

coba gunakan fungsi instr?

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

kemudian

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

Memang ini sedikit hack kotor, tapi itu mengurangi peluang untuk injeksi sql. Tetap bekerja di oracle.


Oh, dan saya sadar bahwa itu tidak akan menggunakan indeks
stjohnroe

itu tidak akan berfungsi untuk beberapa string, misalnya, jika string berisi ','.
David Portabella

1

Sormula mendukung operator SQL IN dengan memungkinkan Anda untuk memasok objek java.util.Collection sebagai parameter. Itu menciptakan pernyataan siap dengan? untuk setiap elemen koleksi. Lihat Contoh 4 (SQL dalam contohnya adalah komentar untuk menjelaskan apa yang dibuat tetapi tidak digunakan oleh Sormula).


1

alih-alih menggunakan

SELECT my_column FROM my_table where search_column IN (?)

gunakan Pernyataan Sql sebagai

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

dan

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

atau menggunakan prosedur tersimpan ini akan menjadi solusi terbaik, karena pernyataan sql akan dikompilasi dan disimpan di server DataBase


1

Saya menemukan sejumlah batasan terkait dengan pernyataan yang disiapkan:

  1. Pernyataan yang disiapkan di-cache hanya di dalam sesi yang sama (Postgres), sehingga hanya akan berfungsi dengan pooling koneksi
  2. Banyak pernyataan yang disiapkan berbeda seperti yang diusulkan oleh @BalusC dapat menyebabkan cache melimpahi dan pernyataan yang sebelumnya di-cache akan dihapus
  3. Permintaan harus dioptimalkan dan menggunakan indeks. Kedengarannya jelas, namun misalnya pernyataan APA PUN (ARRAY ...) yang diajukan oleh @Boris di salah satu jawaban teratas tidak dapat menggunakan indeks dan permintaan akan lambat meskipun caching
  4. Pernyataan yang disiapkan cache rencana kueri juga dan nilai aktual dari setiap parameter yang ditentukan dalam pernyataan tidak tersedia.

Di antara solusi yang diajukan, saya akan memilih salah satu yang tidak mengurangi kinerja kueri dan membuat jumlah kueri yang lebih sedikit. Ini akan menjadi # 4 (mengumpulkan beberapa pertanyaan) dari tautan @Don atau menentukan nilai NULL untuk yang tidak dibutuhkan '?' menandai seperti yang diusulkan oleh @Vladimir Dyuzhev


1

Saya baru saja mengerjakan opsi khusus PostgreSQL untuk ini. Ini sedikit peretasan, dan dilengkapi dengan pro dan kontra dan batasannya sendiri, tetapi tampaknya berfungsi dan tidak terbatas pada bahasa pengembangan, platform, atau driver PG tertentu.

Triknya tentu saja adalah menemukan cara untuk melewatkan koleksi panjang nilai yang berubah-ubah sebagai parameter tunggal, dan minta db mengenalinya sebagai beberapa nilai. Solusi yang saya kerjakan adalah membangun string yang dibatasi dari nilai-nilai dalam koleksi, meneruskan string itu sebagai parameter tunggal, dan menggunakan string_to_array () dengan casting yang diperlukan untuk PostgreSQL untuk memanfaatkannya dengan benar.

Jadi jika Anda ingin mencari "foo", "bla", dan "abc", Anda dapat menyatukannya menjadi satu string sebagai: 'foo, blah, abc'. Inilah SQL lurus:

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

Anda jelas akan mengubah pemeran eksplisit menjadi apa pun yang Anda inginkan menjadi array nilai yang dihasilkan menjadi - int, teks, uuid, dll. Dan karena fungsinya mengambil nilai string tunggal (atau dua saya kira, jika Anda ingin menyesuaikan pembatas juga), Anda dapat meneruskannya sebagai parameter dalam pernyataan yang disiapkan:

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

Ini bahkan cukup fleksibel untuk mendukung hal-hal seperti perbandingan SEPERTI:

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

Sekali lagi, tidak diragukan lagi ini adalah peretasan, tetapi ini berfungsi dan memungkinkan Anda untuk tetap menggunakan pernyataan yang telah disiapkan sebelumnya yang mengambil parameter diskrit * ahem * , dengan keamanan yang menyertainya dan (mungkin) manfaat kinerja. Apakah disarankan dan benar-benar performant? Tentu saja, itu tergantung, karena Anda memiliki penguraian string dan kemungkinan casting berlangsung sebelum kueri Anda bahkan berjalan. Jika Anda mengharapkan untuk mengirim tiga, lima, beberapa lusin nilai, tentu, itu mungkin baik-baik saja. Beberapa ribu? Ya, mungkin tidak terlalu banyak. YMMV, batasan dan pengecualian berlaku, tidak ada jaminan tersurat maupun tersirat.

Tapi itu berhasil.


0

Hanya untuk kelengkapan: Selama himpunan nilai tidak terlalu besar, Anda juga bisa membuat pernyataan seperti

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

yang kemudian bisa Anda lewati untuk mempersiapkan (), dan kemudian menggunakan setXXX () dalam satu lingkaran untuk mengatur semua nilai. Ini kelihatannya menjijikkan, tetapi banyak sistem komersial "besar" secara rutin melakukan hal semacam ini sampai mencapai batas spesifik DB, seperti 32 KB (menurut saya) untuk pernyataan di Oracle.

Tentu saja Anda perlu memastikan bahwa set tidak akan pernah terlalu besar, atau melakukan jebakan kesalahan jika itu terjadi.


Ya kau benar. Tujuan saya dalam hal ini adalah untuk menggunakan kembali PreparedStatement dengan jumlah item yang berbeda setiap kali.
Chris Mazzola

3
Menggunakan "ATAU" akan mengaburkan niat. Tetap dengan "IN" karena lebih mudah dibaca dan maksudnya lebih jelas. Satu-satunya alasan untuk beralih adalah jika rencana kueri berbeda.
James Schek

0

Mengikuti ide Adam. Buat pernyataan yang disiapkan Anda pilih my_column dari my_table di mana search_column di (#) Buat sebuah String x dan isi dengan sejumlah "?,?,?" tergantung pada daftar nilai Anda Kemudian ubah saja # dalam kueri untuk String x baru Anda


0

Hasilkan string kueri di PreparedStatement untuk memiliki sejumlah? Yang cocok dengan jumlah item dalam daftar Anda. Ini sebuah contoh:

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
Tidak perlu menggunakan StringBuilder lagi. Kompilator mengubah tanda-tanda + menjadi StringBuilder.append (), jadi tidak ada hit kinerja. Coba sendiri :)
neu242

5
@ neu242: Oh ya, kompilator menggunakan StringBuilder. Tapi tidak dengan cara Anda berpikir. Decompiling generateQsForInAnda dapat melihat bahwa per loop iterasi dua baru StringBuilderdialokasikan dan toStringdisebut pada setiap. The StringBuilderoptimasi hanya menangkap hal-hal seperti "x" + i+ "y" + jtetapi tidak melampaui satu ekspresi.
AH

@ neu242 Tidak bisakah Anda menggunakan ps.setObject(1,items)alih-alih mengulang daftar dan kemudian mengatur paramteres?
Neha Choudhary

0

Ada beberapa pendekatan alternatif yang bisa kita gunakan untuk klausa IN di PreparedStatement.

  1. Menggunakan Permintaan Tunggal - kinerja paling lambat dan sumber daya intensif
  2. Menggunakan StoredProcedure - Tercepat tetapi spesifik basis data
  3. Membuat kueri dinamis untuk PreparedStatement - Performa Baik tetapi tidak mendapatkan manfaat dari caching dan PreparedStatement dikompilasi ulang setiap waktu.
  4. Gunakan NULL dalam kueri PreparedStatement - Performa optimal, bekerja sangat baik bila Anda tahu batas argumen klausa IN. Jika tidak ada batasan, maka Anda bisa menjalankan kueri dalam batch. Cuplikan kode contoh adalah;

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

Anda dapat memeriksa lebih detail tentang pendekatan alternatif ini di sini .


"Membuat kueri dinamis untuk PreparedStatement - Performa Bagus tetapi tidak mendapatkan keuntungan dari caching dan PreparedStatement dikompilasi ulang setiap waktu." caching dan menghindari kompilasi adalah apa yang membuat pernyataan yang disiapkan berkinerja baik. Karena itu, saya tidak setuju dengan klaim Anda. Ini akan, bagaimanapun, mencegah injeksi SQL karena Anda membatasi input gabungan / dinamis ke koma.
Brandon

Saya setuju dengan Anda, namun "Kinerja Bagus" di sini adalah untuk skenario khusus ini. Performanya lebih baik daripada pendekatan 1, namun pendekatan 2 tercepat.
Pankaj

0

Untuk beberapa situasi, regexp mungkin membantu. Berikut adalah contoh yang telah saya periksa di Oracle, dan itu berfungsi.

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

Tapi ada beberapa kelemahannya:

  1. Setiap kolom yang diterapkan harus dikonversi ke varchar / char, setidaknya secara implisit.
  2. Perlu hati-hati dengan karakter khusus.
  3. Ini dapat memperlambat kinerja - dalam kasus saya versi IN menggunakan pemindaian indeks dan jangkauan, dan versi REGEXP melakukan pemindaian penuh.

0

Setelah memeriksa berbagai solusi di forum yang berbeda dan tidak menemukan solusi yang baik, saya merasa hack di bawah ini yang saya buat, adalah yang termudah untuk diikuti dan kode:

Contoh: Misalkan Anda memiliki beberapa parameter untuk lulus dalam klausa 'IN'. Cukup masukkan String boneka di dalam klausa 'IN', katakan, "PARAM" menunjukkan daftar parameter yang akan datang di tempat String boneka ini.

    select * from TABLE_A where ATTR IN (PARAM);

Anda dapat mengumpulkan semua parameter menjadi variabel String tunggal dalam kode Java Anda. Hal ini dapat dilakukan sebagai berikut:

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

Anda dapat menambahkan semua parameter Anda dipisahkan dengan koma menjadi variabel String tunggal, 'param1', dalam kasus kami.

Setelah mengumpulkan semua parameter menjadi String tunggal, Anda bisa mengganti teks dummy dalam kueri Anda, yaitu, "PARAM" dalam kasus ini, dengan parameter String, yaitu, param1. Inilah yang perlu Anda lakukan:

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

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

Anda sekarang dapat menjalankan kueri Anda menggunakan metode executeQuery (). Pastikan Anda tidak memiliki kata "PARAM" di kueri Anda di mana pun. Anda dapat menggunakan kombinasi karakter dan huruf khusus alih-alih kata "PARAM" untuk memastikan bahwa tidak ada kemungkinan kata seperti itu masuk dalam kueri. Semoga Anda mendapatkan solusinya.

Catatan: Meskipun ini bukan kueri yang disiapkan, ia melakukan pekerjaan yang saya ingin kode saya lakukan.


0

Hanya untuk kelengkapan dan karena saya tidak melihat orang lain menyarankannya:

Sebelum menerapkan salah satu saran rumit di atas, pertimbangkan apakah injeksi SQL memang merupakan masalah dalam skenario Anda.

Dalam banyak kasus, nilai yang diberikan kepada IN (...) adalah daftar id yang telah dihasilkan sedemikian rupa sehingga Anda dapat memastikan bahwa tidak ada injeksi yang mungkin ... (mis. Hasil pemilihan some_id sebelumnya dari some_table di mana some_condition.)

Jika itu masalahnya Anda mungkin hanya menggabungkan nilai ini dan tidak menggunakan layanan atau pernyataan yang disiapkan untuk itu atau menggunakannya untuk parameter lain dari permintaan ini.

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

0

PreparedStatement tidak menyediakan cara yang baik untuk berurusan dengan klausa SQL IN. Per http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Anda tidak dapat menggantikan hal-hal yang dimaksudkan untuk menjadi bagian dari pernyataan SQL. Ini diperlukan karena jika SQL itu sendiri dapat berubah, Driver tidak dapat mengkompilasi pernyataan. Ini juga memiliki efek samping yang bagus untuk mencegah serangan injeksi SQL. " Saya akhirnya menggunakan pendekatan berikut:

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 adalah solusi terbaik tetapi tidak tersedia untuk banyak driver lama. Solusi berikut ini dapat digunakan di 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);
}

Solusi ini lebih baik daripada solusi loop jelek lainnya di mana string kueri dibangun oleh iterasi manual


0

Ini bekerja untuk saya (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)";
}

spesifik mengikat:

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

Contoh saya untuk database SQLite dan Oracle.

For loop pertama adalah untuk Membuat Objek PreparedStatement.

Untuk loop kedua adalah untuk Memasok Nilai untuk Parameter 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

Solusi saya (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 adalah array yang berisi input / kunci / bidang Anda, dll

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.