PreparedStatement IN mệnh đề thay thế?


342

Cách giải quyết tốt nhất để sử dụng INmệnh đề SQL với các phiên bản của java.sql.PreparedStatement, không được hỗ trợ cho nhiều giá trị do các vấn đề bảo mật tấn công SQL tiêm: Một ?trình giữ chỗ biểu thị một giá trị, thay vì danh sách các giá trị.

Hãy xem xét câu lệnh SQL sau:

SELECT my_column FROM my_table where search_column IN (?)

Sử dụng preparedStatement.setString( 1, "'A', 'B', 'C'" );về cơ bản là một nỗ lực không hoạt động tại một cách giải quyết các lý do để sử dụng ?ở nơi đầu tiên.

Cách giải quyết có sẵn?


1
Oscar, tôi nghĩ rằng thế hệ động của (?,?, ....) là cách giải quyết đơn giản nhất nếu bạn cần một mệnh đề IN, nhưng tôi đã để nó cho các cuộc gọi riêng lẻ vì hiệu suất là đủ trong trường hợp cụ thể của tôi.
Chris Mazzola

6
Một trong những lợi thế của các báo cáo được chuẩn bị là sohuld có thể được biên dịch một lần cho hiệu quả. Bằng cách làm cho mệnh đề in động, điều này phủ nhận một cách hiệu quả câu lệnh đã chuẩn bị.

2
Trên thực tế, điều này hoạt động với MySQL (sử dụng setObject để đặt một mảng String làm giá trị tham số). Bạn đang sử dụng DB gì?
Frans


Đây là một câu hỏi liên quan: stackoverflow.com/q/6956025/521799
Lukas Eder

Câu trả lời:


194

Một phân tích về các tùy chọn khác nhau có sẵn, và ưu và nhược điểm của từng tùy chọn có sẵn ở đây .

Các tùy chọn được đề xuất là:

  • Chuẩn bị SELECT my_column FROM my_table WHERE search_column = ?, thực hiện nó cho từng giá trị và UNION phía khách hàng kết quả. Chỉ yêu cầu một tuyên bố chuẩn bị. Chậm và đau.
  • Chuẩn bị SELECT my_column FROM my_table WHERE search_column IN (?,?,?)và thực hiện nó. Yêu cầu một tuyên bố được chuẩn bị cho mỗi kích thước của danh sách. Nhanh chóng và rõ ràng.
  • Chuẩn bị SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...và thực hiện nó. [Hoặc sử dụng UNION ALLthay cho những dấu chấm phẩy đó. --ed] Yêu cầu một tuyên bố được chuẩn bị cho mỗi kích thước trong danh sách. Hoàn toàn chậm chạp, tệ hơn nhiều WHERE search_column IN (?,?,?), vì vậy tôi không biết tại sao blogger thậm chí còn đề xuất nó.
  • Sử dụng một thủ tục được lưu trữ để xây dựng tập kết quả.
  • Chuẩn bị N truy vấn kích thước trong danh sách khác nhau; nói, với 2, 10 và 50 giá trị. Để tìm kiếm danh sách IN có 6 giá trị khác nhau, hãy điền truy vấn size-10 để nó trông như thế SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Bất kỳ máy chủ phong nha sẽ tối ưu hóa các giá trị trùng lặp trước khi chạy truy vấn.

Không ai trong số các tùy chọn này là siêu tuyệt vời, mặc dù.

Các câu hỏi trùng lặp đã được trả lời ở những nơi này với các lựa chọn thay thế lành mạnh như nhau, vẫn không có câu hỏi nào siêu hay:

Câu trả lời đúng, nếu bạn đang sử dụng JDBC4 và một máy chủ hỗ trợ x = ANY(y), sẽ sử dụng PreparedStatement.setArraynhư được mô tả ở đây:

Dường như không có cách nào để thực hiện setArraycông việc với danh sách IN.


Đôi khi các câu lệnh SQL được tải trong thời gian chạy (ví dụ: từ tệp thuộc tính) nhưng yêu cầu số lượng tham số thay đổi. Trong các trường hợp như vậy, trước tiên hãy xác định truy vấn:

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

Tiếp theo, tải truy vấn. Sau đó xác định số lượng tham số trước khi chạy nó. Khi đã biết số lượng tham số, hãy chạy:

sql = any( sql, count );

Ví dụ:

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

Đối với các cơ sở dữ liệu nhất định khi việc truyền một mảng qua đặc tả JDBC 4 không được hỗ trợ, phương thức này có thể tạo điều kiện chuyển đổi chậm = ?thành IN (?)điều kiện mệnh đề nhanh hơn , sau đó có thể được mở rộng bằng cách gọi anyphương thức.


123

Giải pháp cho 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();
}

hoặc là

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
có vẻ tốt Phần nào của mã này là PostreQuery cụ thể? "nơi search_column = ANY (?)"? hoặc kết nối.createArrayOf? hay cái gì khác?
David Portabella

1
Tôi nghĩ rằng nó đặc biệt hơn JDBC4 so với PostgreSQL, vì .createArrayOf()một phần, nhưng tôi không chắc ngữ nghĩa nghiêm ngặt cho người dùng Arrayđược định nghĩa bởi đặc tả JDBC.
lvella

3
Nếu .createArrayOfkhông hoạt động, bạn có thể tự tạo một mảng theo nghĩa đen của riêng mình String arrayLiteral = "{A,\"B \", C,D}" (lưu ý rằng "B" có khoảng trắng trong khi C không) và sau đó statement.setString(1,arrayLiteral)là câu lệnh được chuẩn bị là ... IN (SELECT UNNEST(?::VARCHAR[]))hay ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: Tôi không nghĩ ANYlàm việc với a SELECT.)
ADTC

Giải pháp tuyệt vời! Thực sự tiết kiệm trong ngày cho tôi. Đối với mảng số nguyên tôi đã sử dụng "int" trong tham số đầu tiên của createdArrayOf () và nó trông thật tuyệt. Tham số đầu tiên đó xuất hiện đặc thù DB, dựa trên tài liệu mặc dù.
Emmanuel Touzery

2
Đây có vẻ là giải pháp sạch nhất. Nếu bất cứ ai đang tìm kiếm cú pháp cụ thể HSQLDB: Tôi đã quản lý để làm điều này hoạt động với IN (UNNEST (?))
aureianimus

19

Không có cách đơn giản AFAIK. Nếu mục tiêu là giữ tỷ lệ bộ đệm câu lệnh cao (nghĩa là không tạo câu lệnh cho mỗi số tham số), bạn có thể làm như sau:

  1. tạo một câu lệnh với một vài (ví dụ 10) tham số:

    ... Ở ĐÂU (?,?,?,?,?,?,?,?,?,?) ...

  2. Ràng buộc tất cả các tham số Actuall

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

  3. Ràng buộc phần còn lại là NULL

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

NULL không bao giờ khớp với bất cứ thứ gì, do đó, nó được tối ưu hóa bởi trình xây dựng kế hoạch SQL.

Logic rất dễ tự động hóa khi bạn chuyển Danh sách vào hàm 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 không bao giờ khớp với bất cứ thứ gì" - NULLTrong truy vấn có khớp với NULLgiá trị trong cơ sở dữ liệu không?
Craig McQueen

5
@CraigMcQueen Không, không. Null thậm chí không khớp với null, theo tiêu chuẩn ANSI.
Dawood ibn Kareem

Bạn có thể kết hợp NULL bằng cách sử dụng từ khóa IS NULL. Một cách hay để phát hiện các hàng không tồn tại trong bảng đã tham gia là sử dụng LEFT THAM GIA cùng với IS NULL. 'CHỌN a.URL, b.URL TỪ TABLE_A a TÌM HIỂU TABLE_B b TRÊN a_A.URL = b_B.URL WHERE b.URL IS NULL' Điều này sẽ hiển thị tất cả các hàng trong bảng A không khớp trong bảng B.
Jens Tandstad

3
Hãy cẩn thận với điều này mặc dù. NOT ININkhông xử lý null theo cùng một cách. Chạy cái này và xem điều gì xảy ra: select 'Matched' as did_it_match where 1 not in (5, null); Sau đó loại bỏ nullvà xem phép thuật.
Brandon

Hoặc bạn có thể đặt tất cả các thông số bổ sung thành giá trị của bất kỳ thông số nào trước đó. Bất kỳ công cụ DB tốt sẽ lọc chúng ra. Như vậy a IN (1,2,3,3,3,3,3)là giống như a IN (1,2,3). Nó cũng hoạt động với NOT INkhông giống như a NOT IN (1,2,3,null,null,null,null)(luôn trả về không có hàng như any_value != NULLluôn luôn sai).
Ruslan Stelmachenko

11

Một công việc khó chịu xung quanh, nhưng chắc chắn khả thi là sử dụng truy vấn lồng nhau. Tạo một bảng tạm thời MYVALUES với một cột trong đó. Chèn danh sách các giá trị của bạn vào bảng MYVALUES. Sau đó thực hiện

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

Xấu xí, nhưng một sự thay thế khả thi nếu danh sách các giá trị của bạn rất lớn.

Kỹ thuật này có thêm lợi thế của các gói truy vấn có khả năng tốt hơn từ trình tối ưu hóa (kiểm tra một trang để biết nhiều giá trị, chỉ quét một lần thay vì một lần cho mỗi giá trị, v.v.) có thể tiết kiệm chi phí nếu cơ sở dữ liệu của bạn không lưu các câu lệnh được chuẩn bị. "INSERTS" của bạn sẽ cần phải được thực hiện theo đợt và bảng MYVALUES có thể cần được điều chỉnh để có khóa tối thiểu hoặc các biện pháp bảo vệ trên cao khác.


Những lợi thế nào có thể vượt qua truy vấn my_table một giá trị tại một thời điểm?
Paul Tomblin

3
Trình tối ưu hóa truy vấn có thể giảm tải I / O bằng cách truy xuất tất cả các kết quả khớp có thể từ một trang được tải. Quét bảng hoặc quét chỉ mục có thể được thực hiện một lần thay vì một lần cho mỗi giá trị. Chi phí để chèn giá trị có thể được giảm với các hoạt động hàng loạt và có thể ít hơn một số truy vấn.
James Schek

1
nó có vẻ tốt, nhưng có thể có vấn đề với sự tương tranh. đặc tả jdbc có tạo ra một cách để tạo một bảng ẩn danh tạm thời trong bộ nhớ không? hoặc một cái gì đó như thế, nếu có thể không cụ thể nhà cung cấp jdbc?
David Portabella

9

Hạn chế của toán tử in () là gốc của mọi tội lỗi.

Nó hoạt động cho các trường hợp tầm thường và bạn có thể mở rộng nó bằng "thế hệ tự động của câu lệnh được chuẩn bị" tuy nhiên nó luôn có giới hạn của nó.

  • nếu bạn đang tạo một câu lệnh với số lượng tham số thay đổi, điều đó sẽ khiến chi phí phân tích cú pháp sql ở mỗi cuộc gọi
  • trên nhiều nền tảng, số lượng tham số của toán tử in () bị giới hạn
  • trên tất cả các nền tảng, tổng kích thước văn bản SQL bị giới hạn, không thể gửi 2000 chỗ dành cho các thông số
  • gửi các biến liên kết 1000-10k là không thể, vì trình điều khiển JDBC đang có những hạn chế

Cách tiếp cận in () có thể đủ tốt cho một số trường hợp, nhưng không phải bằng chứng tên lửa :)

Giải pháp chống tên lửa là truyền số lượng tham số tùy ý trong một cuộc gọi riêng (ví dụ: bằng cách chuyển một cụm thông số), sau đó có chế độ xem (hoặc bất kỳ cách nào khác) để thể hiện chúng trong SQL và sử dụng ở nơi bạn tiêu chí.

Một biến thể vũ phu có ở đây http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Tuy nhiên nếu bạn có thể sử dụng PL / SQL, mớ hỗn độn này có thể trở nên khá gọn gàng.

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;

Sau đó, bạn có thể chuyển số id khách hàng được phân tách bằng dấu phẩy tùy ý trong tham số và:

  • sẽ không có độ trễ phân tích, vì SQL để chọn ổn định
  • không phức tạp về chức năng của đường ống - nó chỉ là một truy vấn
  • SQL đang sử dụng một phép nối đơn giản, thay vì toán tử IN, khá nhanh
  • xét cho cùng, đó là một quy tắc tốt khi không đánh vào cơ sở dữ liệu với bất kỳ lựa chọn đơn giản hoặc DML nào, vì đó là Oracle, nơi cung cấp nhiều ánh sáng hơn MySQL hoặc các công cụ cơ sở dữ liệu đơn giản tương tự. PL / SQL cho phép bạn ẩn mô hình lưu trữ khỏi mô hình miền ứng dụng của bạn một cách hiệu quả.

Mẹo ở đây là:

  • chúng ta cần một cuộc gọi chấp nhận chuỗi dài và lưu trữ ở đâu đó nơi phiên db có thể truy cập vào chuỗi đó (ví dụ: biến gói đơn giản hoặc dbms_session.set_context)
  • sau đó chúng ta cần một khung nhìn có thể phân tích cú pháp này thành các hàng
  • và sau đó bạn có một chế độ xem chứa các id bạn đang truy vấn, vì vậy tất cả những gì bạn cần là một phép nối đơn giản với bảng được truy vấn.

Khung nhìn trông như sau:

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

trong đó aux_in_list.getpayload đề cập đến chuỗi đầu vào ban đầu.


Một cách tiếp cận khả thi sẽ là vượt qua các mảng pl / sql (chỉ được hỗ trợ bởi Oracle), tuy nhiên bạn không thể sử dụng các mảng trong SQL thuần, do đó luôn cần một bước chuyển đổi. Việc chuyển đổi không thể được thực hiện trong SQL, do đó, sau tất cả, chuyển một clob với tất cả các tham số trong chuỗi và chuyển đổi nó với một khung nhìn là giải pháp hiệu quả nhất.


6

Đây là cách tôi giải quyết nó trong ứng dụng của riêng tôi. Tốt nhất, bạn nên sử dụng StringBuilder thay vì sử dụng + cho String.

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

Sử dụng một biến như x ở trên thay vì số cụ thể sẽ giúp ích rất nhiều nếu bạn quyết định thay đổi truy vấn sau đó.


đã cố gắng tương tự, không làm việc cho Oracle
Santosh Raviteja

5

Tôi chưa bao giờ thử nó, nhưng .setArray () sẽ làm những gì bạn đang tìm kiếm?

Cập nhật : Rõ ràng là không. setArray dường như chỉ hoạt động với java.sql.Array xuất phát từ cột ARRAY mà bạn đã truy xuất từ ​​truy vấn trước đó hoặc truy vấn con với cột ARRAY.


4
Không hoạt động với tất cả các cơ sở dữ liệu, nhưng đó là cách tiếp cận "chính xác".
skaffman

Bạn có nghĩa là tất cả các trình điều khiển. Một số trình điều khiển có tương đương độc quyền của tiêu chuẩn năm nay (thế kỷ trước?). Một cách khác là gộp một loạt các giá trị vào một bảng tạm thời, nhưng không phải tất cả các cơ sở dữ liệu đều hỗ trợ ...
Tom Hawtin - tackline

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/ Khăn Theo Sun, nội dung mảng [thường] vẫn ở phía máy chủ và được kéo khi cần. PreparedStatement.setArray () có thể gửi lại một Mảng từ một Kết quả trước đó, không tạo ra một Mảng mới ở phía máy khách.
Chris Mazzola

5

Cách giải quyết của tôi là:

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

Bây giờ bạn có thể sử dụng một biến để lấy một số giá trị trong bảng:

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

Vì vậy, tuyên bố chuẩn bị có thể là:

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

Trân trọng,

Javier Ibanez


Đây là PL / SQL, vâng. Nó wonát làm việc trong các cơ sở dữ liệu khác. Lưu ý rằng việc triển khai này có giới hạn các tham số đầu vào - tổng chiều dài được giới hạn ở 32k ký tự -, cũng như giới hạn hiệu suất do lệnh gọi đến hàm pipelined thực hiện chuyển đổi ngữ cảnh giữa các công cụ PL / SQL và SQL của Oracle.
Gee Bee

3

Tôi cho rằng bạn có thể (sử dụng thao tác chuỗi cơ bản) tạo chuỗi truy vấn trong PreparedStatementđể có số lượng mục ?khớp với số mục trong danh sách của bạn.

Tất nhiên, nếu bạn đang làm điều đó chỉ là một bước để tạo ra một chuỗi khổng lồ ORtrong truy vấn của bạn, nhưng không có số lượng đúng ?trong chuỗi truy vấn, tôi không thấy bạn có thể làm việc khác như thế nào.


Không thực sự là một giải pháp cho tôi vì tôi muốn gửi một số lượng khác nhau? mỗi lần tôi gọi ps. Nhưng đừng nghĩ rằng tôi đã không xem xét nó. : P
Chris Mazzola

4
Một cách hack khác: bạn có thể sử dụng một số lượng lớn các trình giữ chỗ tham số - bao nhiêu danh sách giá trị dài nhất bạn sẽ có - và nếu danh sách các giá trị của bạn ngắn hơn, bạn có thể lặp lại các giá trị: ... WHERE searchfield IN (? ,?,?,?,?,?,?,?) Và sau đó cung cấp các giá trị: A, B, C, D, A, B, C, D
Bill Karwin

1
Nhưng nhìn chung, tôi ủng hộ giải pháp của Adam: tạo SQL một cách linh hoạt và ghép nối? giữ chỗ để phù hợp với số lượng giá trị bạn phải vượt qua.
Bill Karwin

Bill, giải pháp đó hoàn toàn khả thi nếu tôi không muốn sử dụng lại PreparedStatement. Một giải pháp khác là thực hiện cuộc gọi param duy nhất nhiều lần và tích lũy kết quả về phía máy khách. Có thể sẽ hiệu quả hơn khi xây dựng / thực thi Tuyên bố mới với số lượng tùy chỉnh? mỗi lần mặc dù.
Chris Mazzola

3

Bạn có thể sử dụng phương thức setArray như được đề cập trong javadoc này :

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
điều này không được hỗ trợ bởi tất cả các trình điều khiển, nếu tính năng này không được hỗ trợ, bạn sẽ nhận được SQLFeatureNotSupportedException
chưa được đặt tên

Thật không may, trình điều khiển của tôi không hỗ trợ nó
EdXX

Điều này không hoạt động cho Oracle
Santosh Raviteja

3

Bạn có thể sử dụng Collections.nCopiesđể tạo một bộ sưu tập các trình giữ chỗ và tham gia chúng bằng cách sử dụng 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
     */
}

Có vẻ là giải pháp tốt nhất cho đến nay khi sử dụng Oracle JDBC ...
jansohn

2

Đây là một giải pháp hoàn chỉnh trong Java để tạo câu lệnh chuẩn bị cho bạn:

/*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 cho phép chuyển java.util.Lists sang NamedParameterJdbcTemplate , tự động hóa việc tạo (?,?,?, ...,?), Phù hợp với số lượng đối số.

Đối với Oracle, bài đăng trên blog này thảo luận về việc sử dụng oracle.sql.ARRAY (Connection.createArrayOf không hoạt động với Oracle). Đối với điều này, bạn phải sửa đổi câu lệnh SQL của bạn:

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

Hàm bảng oracle biến đổi mảng đã truyền thành một bảng giống như giá trị có thể sử dụng được trong INcâu lệnh.


1

Hãy thử sử dụng chức năng Bars?

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

sau đó

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

Phải thừa nhận rằng đây là một chút hack bẩn, nhưng nó làm giảm cơ hội tiêm sql. Dù sao cũng hoạt động trong nhà tiên tri.


Ồ, và tôi biết rằng nó sẽ không sử dụng các chỉ mục
stjohnroe

chẳng hạn, nó sẽ không hoạt động đối với một số chuỗi, nếu chuỗi chứa ','.
David Portabella

1

Sormula hỗ trợ toán tử SQL IN bằng cách cho phép bạn cung cấp một đối tượng java.util.Collection làm tham số. Nó tạo ra một tuyên bố chuẩn bị với một? cho mỗi yếu tố bộ sưu tập. Xem ví dụ 4 (ví dụ SQL là một nhận xét để làm rõ những gì được tạo nhưng không được Sormula sử dụng).


1

thay vì sử dụng

SELECT my_column FROM my_table where search_column IN (?)

sử dụng câu lệnh Sql như

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

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

hoặc sử dụng một thủ tục được lưu trữ, đây sẽ là giải pháp tốt nhất, vì các câu lệnh sql sẽ được biên dịch và lưu trữ trong máy chủ DataBase


1

Tôi đã gặp một số hạn chế liên quan đến tuyên bố đã chuẩn bị:

  1. Các báo cáo được chuẩn bị chỉ được lưu trong bộ nhớ cache trong cùng một phiên (Postgres), vì vậy nó sẽ thực sự chỉ hoạt động với nhóm kết nối
  2. Rất nhiều câu lệnh được chuẩn bị khác nhau theo đề xuất của @BalusC có thể khiến bộ đệm bị lấp đầy và các câu lệnh được lưu trong bộ nhớ cache trước đó sẽ bị hủy
  3. Các truy vấn phải được tối ưu hóa và sử dụng các chỉ số. Tuy nhiên, âm thanh rõ ràng, ví dụ: câu lệnh BẤT K ((ARRAY ...) được đề xuất bởi @Boris trong một trong những câu trả lời hàng đầu không thể sử dụng chỉ mục và truy vấn sẽ bị chậm mặc dù bộ đệm
  4. Câu lệnh được chuẩn bị cũng lưu trữ kế hoạch truy vấn và các giá trị thực tế của bất kỳ tham số nào được chỉ định trong câu lệnh đều không khả dụng.

Trong số các giải pháp được đề xuất, tôi sẽ chọn giải pháp không làm giảm hiệu năng truy vấn và làm cho số lượng truy vấn ít hơn. Đây sẽ là số 4 (một vài truy vấn) từ liên kết @Don hoặc chỉ định các giá trị NULL cho không cần thiết '?' đánh dấu theo đề xuất của @Vladimir Dyuzhev


1

Tôi vừa thực hiện một tùy chọn dành riêng cho PostgreSQL cho việc này. Đó là một chút hack, đi kèm với những ưu và nhược điểm và hạn chế riêng của nó, nhưng nó dường như hoạt động và không giới hạn ở một ngôn ngữ phát triển, nền tảng hoặc trình điều khiển PG cụ thể.

Thủ thuật tất nhiên là tìm cách vượt qua một tập hợp các giá trị độ dài tùy ý dưới dạng một tham số duy nhất và để db nhận ra nó là nhiều giá trị. Giải pháp tôi đang làm là xây dựng một chuỗi được phân tách từ các giá trị trong bộ sưu tập, chuyển chuỗi đó thành một tham số duy nhất và sử dụng chuỗi_to_array () với phép truyền cần thiết cho PostgreQuery để sử dụng đúng cách.

Vì vậy, nếu bạn muốn tìm kiếm "foo", "blah" và "abc", bạn có thể ghép chúng lại với nhau thành một chuỗi như: 'foo, blah, abc'. Đây là SQL thẳng:

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

Rõ ràng là bạn sẽ thay đổi biểu mẫu rõ ràng thành bất cứ thứ gì bạn muốn mảng giá trị kết quả của bạn là - int, text, uuid, v.v. Và bởi vì hàm đang lấy một giá trị chuỗi đơn (hoặc hai tôi cho rằng, nếu bạn muốn tùy chỉnh dấu phân cách cũng vậy), bạn có thể truyền nó dưới dạng tham số trong câu lệnh đã chuẩn bị:

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

Điều này thậm chí đủ linh hoạt để hỗ trợ những thứ như so sánh THÍCH:

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

Một lần nữa, không có câu hỏi nào là hack, nhưng nó hoạt động và cho phép bạn vẫn sử dụng các câu lệnh được chuẩn bị trước được biên dịch sẵn * ahem * các tham số rời rạc , với các lợi ích bảo mật và (có thể) hiệu suất đi kèm. Có nên và thực sự hiệu quả? Tất nhiên, điều đó phụ thuộc, vì bạn đã phân tích chuỗi và có thể truyền diễn ra trước khi truy vấn của bạn thậm chí chạy. Nếu bạn đang mong đợi gửi ba, năm, vài chục giá trị, chắc chắn, nó có thể tốt. Vài ngàn? Vâng, có lẽ không quá nhiều. YMMV, giới hạn và loại trừ được áp dụng, không có bảo hành rõ ràng hay ngụ ý.

Nhưng nó đã có tác dụng.


0

Chỉ để hoàn thiện: Miễn là tập hợp các giá trị không quá lớn, bạn cũng có thể chỉ cần xây dựng chuỗi một câu lệnh như

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

mà sau đó bạn có thể chuyển để chuẩn bị () và sau đó sử dụng setXXX () trong một vòng lặp để đặt tất cả các giá trị. Điều này có vẻ xui xẻo, nhưng nhiều hệ thống thương mại "lớn" thường làm điều này cho đến khi chúng đạt đến giới hạn cụ thể của DB, chẳng hạn như 32 KB (tôi nghĩ là vậy) cho các tuyên bố trong Oracle.

Tất nhiên bạn cần đảm bảo rằng tập hợp sẽ không bao giờ lớn một cách vô lý, hoặc thực hiện bẫy lỗi trong trường hợp nó xảy ra.


Vâng bạn đã đúng. Mục tiêu của tôi trong trường hợp này là sử dụng lại PreparedStatement với số lượng vật phẩm khác nhau mỗi lần.
Chris Mazzola

3
Sử dụng "HOẶC" sẽ làm xáo trộn ý định. Gắn với "IN" vì nó dễ đọc hơn và ý định rõ ràng hơn. Lý do duy nhất để chuyển đổi là nếu các kế hoạch truy vấn khác nhau.
James Schek

0

Theo ý tưởng của Adam. Đặt câu lệnh đã chuẩn bị của bạn chọn my_column từ my_table trong đó search_column trong (#) Tạo Chuỗi x và điền nó với một số "?,?,?" tùy thuộc vào danh sách các giá trị của bạn Sau đó, chỉ cần thay đổi # trong truy vấn cho Chuỗi mới x của bạn


0

Tạo chuỗi truy vấn trong PreparedStatement để có số lượng phù hợp với số lượng mục trong danh sách của bạn. Đây là một ví dụ:

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
Không cần sử dụng StringBuilder nữa. Trình biên dịch chuyển đổi các dấu + thành StringBuilder.append (), do đó không có hiệu năng. Hãy tự thử :)
neu242

5
@ neu242: Ồ vâng, trình biên dịch sử dụng StringBuilder. Nhưng không phải theo cách bạn nghĩ. Dịch ngược generateQsForInbạn có thể thấy rằng mỗi vòng lặp hai vòng lặp mới StringBuilderđược phân bổ và toStringđược gọi trên mỗi vòng lặp . Việc StringBuildertối ưu hóa chỉ bắt được những thứ như "x" + i+ "y" + jnhưng không vượt quá một biểu thức.
AH

@ neu242 Bạn không thể sử dụng ps.setObject(1,items)thay vì lặp qua danh sách và sau đó đặt paramteres?
Neha Choudhary

0

Có nhiều cách tiếp cận khác nhau mà chúng ta có thể sử dụng cho mệnh đề IN trong PreparedStatement.

  1. Sử dụng truy vấn đơn - hiệu suất chậm nhất và tốn nhiều tài nguyên
  2. Sử dụng StoredProcedure - Nhanh nhất nhưng cơ sở dữ liệu cụ thể
  3. Tạo truy vấn động cho PreparedStatement - Hiệu suất tốt nhưng không nhận được lợi ích của bộ nhớ đệm và PreparedStatement được biên dịch lại mỗi lần.
  4. Sử dụng NULL trong các truy vấn PreparedStatement - Hiệu suất tối ưu, hoạt động tuyệt vời khi bạn biết giới hạn của các đối số mệnh đề IN. Nếu không có giới hạn, thì bạn có thể thực hiện các truy vấn theo lô. Đoạn mã mẫu là;

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

Bạn có thể kiểm tra chi tiết hơn về các phương pháp thay thế ở đây .


"Tạo truy vấn động cho PreparedStatement - Hiệu suất tốt nhưng không nhận được lợi ích của bộ nhớ đệm và PreparedStatement được biên dịch lại mỗi lần." bộ nhớ đệm và tránh biên dịch lại là những gì làm cho một tuyên bố chuẩn bị thực hiện tốt. Do đó, tôi không đồng ý với yêu cầu của bạn. Tuy nhiên, điều này sẽ ngăn việc tiêm SQL vì bạn đang giới hạn đầu vào động / nối vào dấu phẩy.
Brandon

Tôi đồng ý với bạn, tuy nhiên "Hiệu suất tốt" ở đây dành cho kịch bản cụ thể này. Nó hoạt động tốt hơn phương pháp 1, tuy nhiên cách tiếp cận 2 là nhanh nhất.
Pankaj

0

Đối với một số tình huống regrec có thể giúp đỡ. Đây là một ví dụ tôi đã kiểm tra trên Oracle và nó hoạt động.

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

Nhưng có một số nhược điểm với nó:

  1. Bất kỳ cột nào nó được áp dụng nên được chuyển đổi thành varchar / char, ít nhất là ngầm.
  2. Cần phải cẩn thận với các ký tự đặc biệt.
  3. Nó có thể làm chậm hiệu suất - trong trường hợp của tôi, phiên bản IN sử dụng quét chỉ mục và phạm vi quét và phiên bản REGEXP thực hiện quét toàn bộ.

0

Sau khi kiểm tra các giải pháp khác nhau trong các diễn đàn khác nhau và không tìm thấy giải pháp tốt, tôi cảm thấy bản hack dưới đây tôi nghĩ ra, là cách dễ nhất để theo dõi và mã:

Ví dụ: Giả sử bạn có nhiều tham số cần truyền trong mệnh đề 'IN'. Chỉ cần đặt một chuỗi giả trong mệnh đề 'IN', giả sử "PARAM" biểu thị danh sách các tham số sẽ xuất hiện ở vị trí của Chuỗi giả này.

    select * from TABLE_A where ATTR IN (PARAM);

Bạn có thể thu thập tất cả các tham số thành một biến Chuỗi duy nhất trong mã Java của mình. Điều này có thể được thực hiện như sau:

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

Bạn có thể nối tất cả các tham số được phân tách bằng dấu phẩy vào một biến Chuỗi duy nhất, 'param1', trong trường hợp của chúng tôi.

Sau khi thu thập tất cả các tham số vào một Chuỗi, bạn chỉ có thể thay thế văn bản giả trong truy vấn của mình, tức là "PARAM" trong trường hợp này, bằng Chuỗi tham số, tức là param1. Dưới đây là những gì bạn cần làm:

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

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

Bây giờ bạn có thể thực hiện truy vấn của mình bằng phương thức execQuery (). Chỉ cần đảm bảo rằng bạn không có từ "PARAM" trong truy vấn của mình ở bất cứ đâu. Bạn có thể sử dụng kết hợp các ký tự đặc biệt và bảng chữ cái thay vì từ "PARAM" để đảm bảo rằng không có khả năng có một từ như vậy xuất hiện trong truy vấn. Hy vọng bạn có giải pháp.

Lưu ý: Mặc dù đây không phải là một truy vấn được chuẩn bị, nhưng nó thực hiện công việc mà tôi muốn mã của mình thực hiện.


0

Chỉ vì sự hoàn chỉnh và vì tôi không thấy ai khác đề nghị cả:

Trước khi thực hiện bất kỳ đề xuất phức tạp nào ở trên, hãy xem xét liệu SQL có thực sự là một vấn đề trong kịch bản của bạn không.

Trong nhiều trường hợp, giá trị được cung cấp cho IN (...) là một danh sách các id đã được tạo theo cách mà bạn có thể chắc chắn rằng không thể tiêm được ... (ví dụ: kết quả của một lần trước chọn some_id từ some_table trong đó một số điều kiện.)

Nếu đó là trường hợp bạn có thể chỉ ghép giá trị này và không sử dụng các dịch vụ hoặc câu lệnh đã chuẩn bị cho nó hoặc sử dụng chúng cho các tham số khác của truy vấn này.

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

0

PreparedStatement không cung cấp bất kỳ cách tốt nào để xử lý mệnh đề SQL IN. Per http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 "Bạn không thể thay thế những thứ có nghĩa là trở thành một phần của câu lệnh SQL. Điều này là cần thiết bởi vì nếu chính SQL có thể thay đổi, thì trình điều khiển không thể biên dịch lại câu lệnh. Nó cũng có tác dụng phụ tốt đẹp trong việc ngăn chặn các cuộc tấn công tiêm nhiễm SQL. " Tôi đã kết thúc bằng cách sử dụng phương pháp sau đây:

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 là giải pháp tốt nhất nhưng nó không có sẵn cho nhiều trình điều khiển cũ. Cách giải quyết sau đây có thể được sử dụng trong 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);
}

Giải pháp này tốt hơn các giải pháp vòng lặp xấu xí khác trong đó chuỗi truy vấn được xây dựng bằng các lần lặp thủ công


0

Điều này làm việc cho tôi (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)";
}

chỉ định ràng buộc:

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

Ví dụ của tôi cho cơ sở dữ liệu SQLite và Oracle.

Vòng lặp For đầu tiên là để tạo một đối tượng PreparedStatement.

Vòng lặp For thứ hai là để cung cấp các giá trị cho các tham số 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

Cách giải quyết của tôi (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 là mảng chứa đầu vào / khóa / trường của bạn, v.v.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.