对IN
实例使用SQL java.sql.PreparedStatement
子句的最佳解决方法是什么,由于SQL注入攻击安全问题而导致多个值不支持这种解决方法:一个?
占位符代表一个值,而不是值列表。
考虑以下SQL语句:
SELECT my_column FROM my_table where search_column IN (?)
使用preparedStatement.setString( 1, "'A', 'B', 'C'" );
本质上是一种非工作尝试,首先解释了使用?
的原因。
有哪些可用的解决方法?
答案 0 :(得分:177)
对可用的各种选项进行分析,并提供各自的优缺点here。
建议的选项是:
SELECT my_column FROM my_table WHERE search_column = ?
,为每个值执行它,并在结果客户端执行UNION。只需要一份准备好的声明。缓慢而痛苦。SELECT my_column FROM my_table WHERE search_column IN (?,?,?)
并执行它。每个IN-list大小需要一个准备好的语句。快速而明显。SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...
并执行它。 [或者使用UNION ALL
代替那些分号。 - 每个大小的IN列表需要一个准备好的语句。愚蠢的慢,严格比WHERE search_column IN (?,?,?)
严重,所以我不知道为什么博主甚至建议它。SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6)
。任何体面的服务器都会在运行查询之前优化重复值。但这些选项都不是超级棒。
在这些地方已经回答了重复的问题,同样理智的选择,但仍然没有一个非常棒:
正确答案,如果您使用的是JDBC4和支持x = ANY(y)
的服务器,则使用此处所述的PreparedStatement.setArray
:
但似乎没有办法让setArray
与IN列表一起使用。
有时SQL语句在运行时加载(例如,从属性文件加载),但需要可变数量的参数。在这种情况下,首先定义查询:
query=SELECT * FROM table t WHERE t.column IN (?)
接下来,加载查询。然后在运行之前确定参数的数量。参数计数一旦知道,运行:
sql = any( sql, count );
例如:
/**
* 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规范传递数组的数据库不受支持,此方法可以帮助将慢= ?
转换为更快的IN (?)
子句条件,然后可以通过调用{{}}来扩展它。 {1}}方法。
答案 1 :(得分:112)
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();
}
或
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();
}
答案 2 :(得分:18)
没有简单的方法AFAIK。 如果目标是保持语句高速缓存比率高(即不为每个参数计数创建语句),则可以执行以下操作:
使用一些(例如10个)参数创建一个语句:
......在哪里(?,?,?,?,?,?,?,?,?,?)......
绑定所有实际参数
了setString(1, “富”); 的SetString(2, “巴”);
将其余部分绑定为NULL
的setNull(3,Types.VARCHAR) ... 的setNull(10,Types.VARCHAR)
NULL永远不会匹配任何内容,因此它会被SQL计划构建器优化。
将List传递给DAO函数时,逻辑很容易自动化:
while( i < param.size() ) {
ps.setString(i+1,param.get(i));
i++;
}
while( i < MAX_PARAMS ) {
ps.setNull(i+1,Types.VARCHAR);
i++;
}
答案 3 :(得分:10)
令人不快的解决方法,但肯定可行的是使用嵌套查询。创建一个临时表MYVALUES,其中包含一列。将值列表插入MYVALUES表。然后执行
select my_column from my_table where search_column in ( SELECT value FROM MYVALUES )
丑陋,但如果您的值列表非常大,那么这是一个可行的选择。
此技术的另一个优点是,如果数据库不缓存预准备语句,优化器可能会提供更好的查询计划(检查页面是否存在多个值,表格只扫描一次,而不是每个值一次,等等)可以节省开销。您的“插入”需要批量完成,MYVALUES表可能需要进行调整以获得最小的锁定或其他高开销保护。
答案 4 :(得分:8)
它适用于琐碎的情况,您可以通过“自动生成预准备语句”来扩展它,但它始终有其限制。
对于某些情况,in()方法可以很好,但不能防火箭:)
防火解决方案是在单独的调用中传递任意数量的参数(例如,通过传递一组参数),然后使用视图(或任何其他方式)在SQL中表示它们并使用在你的标准。
这里有一个蛮力变体http://tkyte.blogspot.hu/2006/06/varying-in-lists.html
但是,如果你可以使用PL / SQL,那么这个烂摊子就会变得非常整洁。
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;
然后,您可以在参数中传递任意数量的逗号分隔客户ID,并且:
这里的诀窍是:
视图如下:
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
其中aux_in_list.getpayload引用原始输入字符串。
一种可能的方法是传递pl / sql数组(仅由Oracle支持),但是您不能在纯SQL中使用它们,因此总是需要转换步骤。转换不能在SQL中完成,所以,毕竟传递一个包含所有参数的clob并在视图中转换它是最有效的解决方案。
答案 5 :(得分:5)
以下是我在自己的应用程序中解决它的方法。理想情况下,您应该使用StringBuilder而不是使用+作为字符串。
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();
}
如果您决定稍后更改查询,则使用上述x之类的变量而不是具体数字会有很大帮助。
答案 6 :(得分:5)
我从来没有尝试过,但是.setArray()会做你想要的吗?
更新:显然不是。 setArray似乎只适用于来自您从先前查询中检索到的ARRAY列的java.sql.Array,或者带有ARRAY列的子查询。
答案 7 :(得分:5)
我的解决方法是:
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;
/
现在您可以使用一个变量来获取表中的某些值:
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
所以,准备好的陈述可能是:
"select * from TABLE where COL in (select * from table(split(?)))"
此致
Javier Ibanez
答案 8 :(得分:3)
我想你可以(使用基本的字符串操作)在PreparedStatement
中生成查询字符串,使得?
的数量与列表中的项目数相匹配。
当然,如果你这样做,你只需要在查询中生成一个巨大的链式OR
,但在查询字符串中没有正确数量的?
,我不明白你还能解决这个问题。
答案 9 :(得分:2)
您可以使用this javadoc中提到的setArray方法:
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();
答案 10 :(得分:1)
Sormula允许您提供java.util.Collection对象作为参数,从而支持SQL IN运算符。它用?创建一个准备好的语句?对于每个元素的集合。请参阅Example 4(示例中的SQL是一条注释,用于阐明Sormula未创建但未使用的内容)。
答案 11 :(得分:1)
而不是使用
SELECT my_column FROM my_table where search_column IN (?)
使用Sql语句作为
select id, name from users where id in (?, ?, ?)
和
preparedStatement.setString( 1, 'A');
preparedStatement.setString( 2,'B');
preparedStatement.setString( 3, 'C');
或使用存储过程这将是最好的解决方案,因为sql语句将被编译并存储在DataBase服务器中
答案 12 :(得分:1)
我遇到了与预备陈述有关的一些限制:
在提议的解决方案中,我会选择不会降低查询性能并减少查询次数的解决方案。这将是来自@Don链接的#4(批处理几个查询)或为不需要的'?'指定NULL值@Vladimir Dyuzhev提出的标记
答案 13 :(得分:1)
尝试使用instr函数?
select my_column from my_table where instr(?, ','||search_column||',') > 0
然后
ps.setString(1, ",A,B,C,");
不可否认,这有点肮脏,但确实减少了sql注入的机会。无论如何都在oracle中工作。
答案 14 :(得分:1)
这是一个完整的Java解决方案,可以为您创建准备好的语句:
/*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;
}
}
答案 15 :(得分:1)
Spring允许passing java.util.Lists to NamedParameterJdbcTemplate,它根据参数的数量自动生成(?,?,?,...,?)。
对于Oracle,this blog posting讨论了oracle.sql.ARRAY的使用(Connection.createArrayOf不适用于Oracle)。为此,您必须修改SQL语句:
SELECT my_column FROM my_table where search_column IN (select COLUMN_VALUE from table(?))
oracle table function将传递的数组转换为可在IN
语句中使用的值的表。
答案 16 :(得分:0)
您可以使用Collections.nCopies
生成占位符集合并使用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
*/
}
答案 17 :(得分:0)
我刚刚为此设计了一个PostgreSQL特定的选项。这有点骇人听闻,并且各有优缺点,但似乎可以使用,并且不仅限于特定的开发语言,平台或PG驱动程序。
当然,技巧是找到一种方法来传递任意长度的值集合作为单个参数,并使数据库将其识别为多个值。我正在工作的解决方案是从集合中的值构造一个定界的字符串,将该字符串作为单个参数传递,并使用string_to_array()和PostgreSQL进行必要的强制转换才能正确使用它。
因此,如果您要搜索“ foo”,“ blah”和“ abc”,则可以将它们连接成一个字符串,例如:“ foo,blah,abc”。这是直接的SQL:
select column from table
where search_column = any (string_to_array('foo,blah,abc', ',')::text[]);
您显然可以将显式类型转换更改为您想要的结果值数组为任何类型-int,text,uuid等。而且由于该函数采用单个字符串值(如果您想这样做,则假设是两个)也可以自定义分隔符),您可以在准备好的语句中将其作为参数传递:
select column from table
where search_column = any (string_to_array($1, ',')::text[]);
这甚至足够灵活,可以支持类似LIKE的比较:
select column from table
where search_column like any (string_to_array('foo%,blah%,abc%', ',')::text[]);
同样,毫无疑问,这是一个hack,但是它可以正常工作,并且允许您仍然使用带有 * ahem * 离散参数的预编译预准备语句,同时具有安全性和(也许)性能优势。它是明智的,并且实际上表现良好吗?自然地,这取决于您是否需要在查询运行之前进行字符串解析和强制转换。当然,如果您希望发送三个,五个,几十个值,那可能很好。几千?是的,也许没有那么多。 YMMV适用限制和排除,不作任何明示或暗示的保证。
但是可以。
答案 18 :(得分:0)
SetArray是最好的解决方案,但它不适用于许多旧驱动程序。可以在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);
}
此解决方案优于其他丑陋的while循环解决方案,其中查询字符串是通过手动迭代构建的
答案 19 :(得分:0)
PreparedStatement没有提供任何处理SQL IN子句的好方法。每http://www.javaranch.com/journal/200510/Journal200510.jsp#a2&#34;您无法替换要成为SQL语句一部分的内容。这是必要的,因为如果SQL本身可以更改,则驱动程序无法预编译该语句。它还具有防止SQL注入攻击的良好副作用。&#34;我最终使用了以下方法:
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);
答案 20 :(得分:0)
似乎还没有其他人建议使用现成的查询构建器,例如 jOOQ 或 QueryDSL 甚至 Criteria Query 管理 dynamic IN
lists框,可能包括对可能出现的所有边缘情况的管理,例如:
IN
列表最多 1000 个元素(与绑定值的数量无关)IN
list padding 解决了这个问题)(免责声明:我为 jOOQ 背后的公司工作)
答案 21 :(得分:0)
仅仅是为了完整性,因为我没有看到其他任何人建议:
在实现上述任何复杂建议之前,请考虑SQL注入是否确实是您的方案中的问题。
在许多情况下,提供给IN(...)的值是一个id列表,这些id是以一种可以确保无法注入的方式生成的...(例如,前一个选择some_id的结果来自some_table,其中some_condition。)
如果是这种情况,您可能只是连接此值而不使用服务或预准备语句,或将它们用于此查询的其他参数。
query="select f1,f2 from t1 where f3=? and f2 in (" + sListOfIds + ");";
答案 22 :(得分:0)
在不同的论坛中检查各种解决方案而没有找到一个好的解决方案之后,我觉得我想出的下面的黑客攻击,是最容易理解的代码:
示例:假设您有多个参数传入&#39; IN&#39;条款。只需将一个虚拟字符串放入&#39; IN&#39;条款,比如,&#34; PARAM&#34; do表示将在此虚拟字符串的位置出现的参数列表。
select * from TABLE_A where ATTR IN (PARAM);
您可以将所有参数收集到Java代码中的单个String变量中。这可以按如下方式完成:
String param1 = "X";
String param2 = "Y";
String param1 = param1.append(",").append(param2);
在我们的案例中,您可以将所有以逗号分隔的参数附加到单个字符串变量&#39; param1&#39;中。
将所有参数收集到一个字符串后,您只需替换查询中的虚拟文本,即&#34; PARAM&#34;在这种情况下,使用参数String,即param1。以下是您需要做的事情:
String query = query.replaceFirst("PARAM",param1); where we have the value of query as
query = "select * from TABLE_A where ATTR IN (PARAM)";
您现在可以使用executeQuery()方法执行查询。只要确保你没有“#34; PARAM&#34;在你的查询任何地方。您可以使用特殊字符和字母的组合,而不是单词&#34; PARAM&#34;为了确保查询中不存在这样的单词。希望你能得到解决方案。
注意:虽然这不是一个准备好的查询,但它完成了我希望我的代码要做的工作。
答案 23 :(得分:0)
对于某些情况,regexp可能有所帮助。 这是我在Oracle上检查过的一个例子,它有效。
select * from my_table where REGEXP_LIKE (search_column, 'value1|value2')
但它有许多缺点:
答案 24 :(得分:0)
我们可以在PreparedStatement中使用不同的替代方法用于IN子句。
在PreparedStatement查询中使用NULL - 当您知道IN子句参数的限制时,最佳性能会很有效。如果没有限制,则可以批量执行查询。 示例代码段是;
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);
}
您可以查看有关这些替代方法的更多详细信息here。
答案 25 :(得分:0)
在PreparedStatement中生成查询字符串,使其数量与列表中的项目数相匹配。这是一个例子:
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;
}
答案 26 :(得分:0)
遵循亚当的想法。从my_table中选择my_column进行预准备语句,其中search_column in(#) 创建一个字符串x并用一些“?,?,?”填充它取决于您的值列表 然后只需在查询中更改新的String x和填充
答案 27 :(得分:0)
只是为了完整性:只要值集不是太大,你就可以简单地字符串构造一个像
这样的语句... WHERE tab.col = ? OR tab.col = ? OR tab.col = ?
然后您可以传递给prepare(),然后在循环中使用setXXX()来设置所有值。这看起来很令人讨厌,但许多“大”商业系统通常都会做这种事情,直到达到特定于数据库的限制,例如Oracle中的语句为32 KB(我认为是这样)。
当然,您需要确保该集合永远不会过大,或者在出现错误时进行错误捕获。
答案 28 :(得分:-1)
这对我有用(伪代码):
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)";
}
指定绑定:
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)
}
答案 29 :(得分:-1)
我的SQLite和Oracle数据库示例。
第一个For循环用于创建PreparedStatement对象。
第二个For循环用于为PreparedStatement参数提供值。
...
TASK [debug] ***********************************************************************************************************
ok: [t00vm1] => {
"msg": "/tmp/.mqc-ansible.t00vm1"
}
...
List<String> roles = Arrays.asList("role1","role2","role3");
Map<String, String> menu = getMenu(conn, roles);
答案 30 :(得分:-2)
我的解决方法(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
是包含输入/键/字段等的数组