PreparedStatement IN子句替代项?


342

结合使用SQL IN子句和实例的最佳解决方法java.sql.PreparedStatement是什么,由于SQL注入攻击安全性问题,多个值不支持该子句:一个?占位符代表一个值,而不是值列表。

考虑以下SQL语句:

SELECT my_column FROM my_table where search_column IN (?)

使用preparedStatement.setString( 1, "'A', 'B', 'C'" );本质上是一种不可行的尝试,它是首先解决使用原因的方法?

有哪些解决方法?


1
奥斯卡,如果您需要IN子句,我认为动态生成(?,?,....)是最简单的解决方法,但是由于在我的特定情况下性能已足够,因此我将其单独留给了个人调用。
克里斯·马佐拉

6
准备好的语句的优点之一是可以为效率而编译一次。通过使in子句动态化,可以有效地消除准备好的语句。

2
实际上,这适用于MySQL(使用setObject将String数组设置为参数值)。您正在使用哪个数据库?
弗朗斯(Frans)2012年


这是一个相关的问题:stackoverflow.com/q/6956025/521799
Lukas Eder 2013年

Answers:


194

此处提供了对各种可用选项的分析,以及每种方法的优缺点。

建议的选项是:

  • 准备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代替那些分号。--ed]每个IN-LIST大小需要一个准备好的语句。愚蠢的慢,严格比差WHERE search_column IN (?,?,?),所以我不知道为什么博主甚至建议这样做。
  • 使用存储过程来构造结果集。
  • 准备N个不同的IN列表大小查询;例如,具有2、10和50个值。要搜索具有6个不同值的IN列表,请填充size-10查询,使其看起来像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规范传递数组的数据库,此方法可以促进将slow = ?转换为fast IN (?)子句条件,然后可以通过调用该any方法进行扩展。


123

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

1
看起来不错。这段代码的哪一部分是PostreSQL特定的?“哪里search_column = ANY(?)”?还是connection.createArrayOf?或者是其他东西?
David Portabella 2012年

1
由于这一.createArrayOf()部分,我认为它比PostgreSQL特定于JDBC4特定,但是我不确定用户的严格语义Array是由JDBC规范定义的。
lvella

3
如果.createArrayOf不起作用,则可以手动创建数组文字,例如String arrayLiteral = "{A,\"B \", C,D}" (请注意,“ B”有空格,而C则没有空格),然后statement.setString(1,arrayLiteral)准备的语句位于... IN (SELECT UNNEST(?::VARCHAR[]))... IN (SELECT UNNEST(CAST(? AS VARCHAR[])))。(PS:我认为不ANY适用SELECT。)
ADTC 2013年

很好的解决方案!真的为我节省了一天。对于整数数组,我在createArrayOf()的第一个参数中使用了“ int”,它看起来不错。不过,根据文档,第一个参数似乎是特定于DB的。
伊曼纽尔·图扎里

2
这似乎是最干净的解决方案。如果有人在寻找HSQLDB特定的语法:我设法使其与IN(UNNEST(?))一起使用
aureianimus 2014年

19

没有简单的方法AFAIK。如果目标是保持较高的语句缓存比率(即不为每个参数计数都创建一条语句),则可以执行以下操作:

  1. 创建带有几个(例如10个)参数的语句:

    ...在哪里(?,?,?,?,?,?,?,?,?,?)...

  2. 绑定所有实际参数

    setString(1,“ foo”); setString(2,“ bar”);

  3. 将其余的绑定为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++;
}

“ NULL永不匹配任何内容” — NULL查询中的NULL值是否与数据库中的值匹配?
Craig McQueen 2013年

5
@CraigMcQueen不,不会。根据ANSI标准,Null甚至不匹配null。
2014年

您可以使用IS NULL关键字来匹配NULL。检测联接表中不存在的行的一种好方法是将LEFT JOIN与IS NULL一起使用。“SELECT a.URL,b.URL FROM TABLE_A左连接表-B B关于a_A.URL = b_B.URL WHERE b.URL IS NULL”这将显示所有表A具有表B中没有匹配的行
延Tandstad 2014年

3
不过要小心。 NOT IN并且IN不能以相同的方式处理null。运行此命令,看看会发生什么:select 'Matched' as did_it_match where 1 not in (5, null); 然后删除null并观看魔术。
布兰登

或者,您可以将所有其他参数设置为任何先前参数的值。任何体面的数据库引擎都会将它们过滤掉。因此a IN (1,2,3,3,3,3,3)与相同a IN (1,2,3)。它也可以与NOT INdifferent不同a NOT IN (1,2,3,null,null,null,null)(总是返回any_value != NULLfalse总是不返回任何行)。
Ruslan Stelmachenko

11

一个不愉快的解决方法,但肯定可行的是使用嵌套查询。创建一个临时表MYVALUES,其中包含一列。将值列表插入MYVALUES表。然后执行

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

丑陋,但是如果您的值列表很大,则是一个可行的选择。

如果您的数据库没有缓存准备好的语句,则此技术的另一个优点是可以从优化器获得更好的查询计划(检查页面中是否有多个值,仅对表进行一次表扫描,而对每个值一次),这样可以节省开销。您的“ INSERTS”将需要成批完成,并且可能需要对MYVALUES表进行调整以使其锁定或其他高开销保护降至最低。


与一次查询my_table一个值相比有什么优势?
Paul Tomblin

3
查询优化器可以通过从已加载的页面检索所有可能的匹配项来减少I / O负载。表扫描或索引扫描可以执行一次,而不是每个值一次。批处理操作可以减少插入值的开销,并且可以少于几个查询。
James Schek

1
看起来不错,但是并发性可能存在问题。jdbc规范是否包含一种在内存中创建临时匿名表的方法?或类似的东西,如果可能的话,不是jdbc-vendor特定的吗?
David Portabella 2012年

9

in()运算符的局限性是万恶之源。

它适用于琐碎的情况,您可以通过“自动生成预处理语句”来扩展它,但是它总是有其局限性。

  • 如果要创建带有可变数量参数的语句,则将在每次调用时进行sql解析开销
  • 在许多平台上,in()运算符的参数数量受到限制
  • 在所有平台上,SQL文本的总大小是有限的,因此无法为in params发送2000个占位符
  • 不能向下发送1000-10k的绑定变量,因为JDBC驱动程序有其局限性

在某些情况下,in()方法可能就足够了,但不能证明是火箭的:)

防止火箭攻击的解决方案是在一个单独的调用中传递任意数量的参数(例如,通过传递参数块),然后使用视图(或任何其他方式)在SQL中表示它们并在您的where中使用标准。

这里是蛮力变体,网址为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,并且:

  • 由于select的SQL是稳定的,因此不会获得解析延迟
  • 没有管道函数的复杂性-这只是一个查询
  • SQL使用简单的联接,而不是IN运算符,这非常快
  • 毕竟,这是一个很好的经验法则使用任何普通选择或DML来访问数据库,因为它是Oracle,比MySQL或类似的简单数据库引擎提供了光年。PL / SQL允许您有效地从应用程序域模型中隐藏存储模型。

这里的窍门是:

  • 我们需要一个接受长字符串的调用,并将其存储在db会话可以访问它的地方(例如,简单的包变量或dbms_session.set_context)
  • 那么我们需要一个可以将其解析为行的视图
  • 然后,您将拥有一个包含要查询的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传递给字符串并在视图中进行转换是最有效的解决方案。


6

这是我在自己的应用程序中解决问题的方法。理想情况下,您应该使用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之类的变量代替具体数字会很有帮助。


尝试过相同的方法,但不适用于Oracle
Santosh Raviteja

5

我从来没有尝试过,但是.setArray()是否可以满足您的需求?

更新:显然不是。setArray似乎只能与java.sql.Array一起使用,该java.sql.Array来自您从上一个查询检索到的ARRAY列,或带有ARRAY列的子查询。


4
并非适用于所有数据库,但这是“正确”的方法。
skaffman's

您的意思是所有驱动程序。一些驱动程序具有本年度(上世纪?)标准的专有等效项。另一种方法是将一批值
绑定

java.sun.com/j2se/1.3/docs/guide/jdbc/getstart / ...根据Sun的说法,[通常]数组内容保留在服务器端,并根据需要拉出。PreparedStatement.setArray()可以从以前的ResultSet发送回一个Array,而不是在客户端上创建新的Array。
克里斯·马佐拉

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)


这是PL / SQL,是的。它不能在其他数据库中工作。请注意,此实现具有输入参数的限制-总长度限制为32k个字符-以及性能限制,因为对流水线函数的调用在Oracle的PL / SQL和SQL引擎之间进行上下文切换。
吉蜜蜂

3

我想您可以(使用基本的字符串操作)在中生成查询字符串PreparedStatement以具有多个?与列表中的项目数匹配的多个。

当然,如果您这样做的话,您距离OR在查询中生成巨型链只有一步之遥,但是?在查询字符串中没有正确数量的巨人时,我看不出还有其他方法可以解决此问题。


对于我来说,这不是真正的解决方案,因为我想发送其他数量的吗?每次我叫ps。但是不要以为我没有考虑过。:P
克里斯·马佐拉

4
另一个技巧:您可以使用大量参数占位符-与您拥有的最长值列表一样多-如果值列表较短,则可以重复值:... WHERE searchfield IN(? ,?,?,?,?,?,?,?),然后提供值:A,B,C,D,A,B,C,D
Bill Karwin

1
但是总的来说,我赞成亚当的解决方案:动态生成SQL,并连接?占位符以匹配您必须传递的值的数量。
Bill Karwin

比尔,如果我不想重用PreparedStatement,那么该解决方案是可行的。另一种解决方案是多次调用单个param,并在客户端累积结果。自定义编号为?的新语句的构建/执行可能会更有效。虽然每次。
克里斯·马佐拉

3

您可以使用此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();

2
这是不是所有的驱动程序支持,如果不支持该功能,您将得到SQLFeatureNotSupportedException
无名

不幸的是,我的驱动程序不支持
EdXX

这对Oracle不起作用
Santosh Raviteja

3

您可以使用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
     */
}

到目前为止,似乎是使用Oracle JDBC时最好的解决方案……
jansohn,

2

这是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;

    }

}

2

Spring允许将java.util.Lists传递给NamedParameterJdbcTemplate,后者根据参数的数量自动生成(?,?,?,...,?)。

对于Oracle,此博客文章讨论了oracle.sql.ARRAY的使用(Connection.createArrayOf不适用于Oracle)。为此,您必须修改您的SQL语句:

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

Oracle表函数变换传递的数组到像在值可用的表IN声明。


1

尝试使用instr函数?

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

然后

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

诚然,这有点肮脏,但是它确实减少了SQL注入的机会。无论如何在oracle中工作。


哦,我知道它不会使用索引
stjohnroe's

它不适用于某些字符串,例如,如果字符串包含“,”。
David Portabella 2012年

1

Sormula通过允许您提供java.util.Collection对象作为参数来支持SQL IN运算符。它创建带有?的准备好的语句。对于每个元素集合。请参见示例4(示例中的SQL是注释,以澄清创建的内容但Sormula并未使用)。


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语句将被编译并存储在数据库服务器中


1

我遇到了一些与准备好的陈述有关的限制:

  1. 准备好的语句仅在同一会话(Postgres)中缓存,因此它仅在连接池中有效
  2. @BalusC提出的许多不同的准备好的语句可能导致缓存溢出,并且先前缓存的语句将被丢弃
  3. 必须优化查询并使用索引。听起来似乎很明显,但是例如@Boris在最热门的答案之一中提出的ANY(ARRAY ...)语句无法使用索引,尽管进行了缓存,查询也会很慢
  4. 准备好的语句也将缓存查询计划,并且该语句中指定的任何参数的实际值都不可用。

在提出的解决方案中,我会选择一种不会降低查询性能并减少查询数量的解决方案。这将是@Don链接中的#4(查询少量查询),或者为不需要的'?'指定NULL值。由@Vladimir Dyuzhev提议的标记


1

我只是为此设计了一个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,限制和排除适用,不作任何明示或暗示的保证。

但这有效。


0

只是为了完整性:只要设定值不是太大,你可能也只是串构造类似的声明

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

然后可以将其传递给prepare(),然后在循环中使用setXXX()设置所有值。这看起来很麻烦,但是许多“大型”商业系统通常会执行这种操作,直到达到特定于数据库的限制,例如Oracle语句的32 KB(我认为是)。

当然,您需要确保集合永远不会过大,或者在发生错误捕获的情况下进行错误捕获。


你是对的。在这种情况下,我的目标是每次将PreparedStatement重复使用不同数量的项目。
克里斯·马佐拉

3
使用“ OR”会混淆意图。坚持使用“ IN”,因为它更易于阅读并且意图更加清晰。切换的唯一原因是查询计划是否不同。
James Schek

0

遵循亚当的想法。使您准备好的语句有点像从my_table中选择my_column,其中(#)中的search_column创建一个字符串x并用数字“?,?,?”填充 取决于您的值列表,然后只需在查询中为新的String x(填充)更改#


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

5
不再需要使用StringBuilder。编译器始终将+符号转换为StringBuilder.append(),因此不会对性能造成任何影响。尝试一下:)
neu242

5
@ neu242:哦,是的,编译器使用StringBuilder。但不是您想的那样。反编译后,generateQsForIn您可以看到每个循环迭代分配了两个StringBuilder变量,并toString在每个循环上调用了两个新变量。该StringBuilder优化只抓住这样的东西"x" + i+ "y" + j,但不超出一个表达。
AH 2012年

@ neu242是否可以使用ps.setObject(1,items)而不是遍历列表然后设置paramteres
Neha Choudhary

0

在PreparedStatement中,可以对IN子句使用不同的替代方法。

  1. 使用单一查询-最慢的性能和资源密集型
  2. 使用StoredProcedure-最快但特定于数据库
  3. 为PreparedStatement创建动态查询-良好的性能,但没有获得缓存的好处,并且PreparedStatement每次都会重新编译。
  4. 在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);
        }

您可以在此处查看有关这些替代方法的更多详细信息。


“为PreparedStatement创建动态查询-性能良好,但不能从缓存中受益,并且PreparedStatement每次都会重新编译。” 缓存和避免重新编译是使准备好的语句执行良好的原因。因此,我不同意您的主张。但是,这将阻止SQL注入,因为您将串联/动态输入限制为逗号。
布兰登

我同意您的意见,但是此处的“良好性能”适用于此特定情况。它比方法1更好,但是方法2最快。
Pankaj 2014年

0

在某些情况下,regexp可能会有所帮助。这是我在Oracle上检查过的示例,它可以正常工作。

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

但是它有很多缺点:

  1. 它应用的任何列都应至少隐式转换为varchar / char。
  2. 需要注意特殊字符。
  3. 它会降低性能-在我的情况下,IN版本使用索引和范围扫描,而REGEXP版本则进行完全扫描。

0

在不同的论坛上研究了各种解决方案并没有找到好的解决方案之后,我觉得我想到的以下技巧最容易遵循和编写:

示例:假设您有多个参数要传入“ IN”子句。只需在'IN'子句中放入一个虚拟字符串,例如,“ PARAM”确实表示将替换该虚拟字符串的参数列表。

    select * from TABLE_A where ATTR IN (PARAM);

您可以将所有参数收集到Java代码中的单个String变量中。可以按照以下步骤进行:

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

在本例中,您可以将所有用逗号分隔的参数附加到单个String变量'param1'中。

将所有参数收集到单个String中后,您只需用参数String即param1替换查询中的伪文本(在这种情况下为“ PARAM”)即可。这是您需要做的:

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

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

现在,您可以使用executeQuery()方法执行查询。只要确保您的查询中没有任何单词“ PARAM”即可。您可以使用特殊字符和字母的组合来代替单词“ PARAM”,以确保查询中不会出现这样的单词。希望您能找到解决方案。

注意:尽管这不是准备好的查询,但它可以完成我希望代码执行的工作。


0

仅出于完整性考虑,因为我没有看到其他人建议这样做:

在实施上述任何复杂建议之前,请考虑您的方案中是否确实存在SQL注入问题。

在许多情况下,提供给IN(...)的值是一列ID的列表,这些ID的生成方式可以确保无法进行注入...(例如,上一个从some_table中选择some_id的结果,其中一些条件。)

如果是这种情况,您可能只是串联了该值,而不使用服务或为其准备的语句,也没有将它们用于此查询的其他参数。

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

0

PreparedStatement没有提供处理SQL IN子句的任何好方法。Per http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 “您不能替换本打算成为SQL语句一部分的内容。这是必要的,因为如果SQL本身可以更改,则驱动程序无法预编译该语句。它还具有防止SQL注入攻击的良好副作用。” 我最终使用以下方法:

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是最好的解决方案,但不适用于许多较旧的驱动程序。以下变通办法可以在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循环解决方案更好,在后者中,查询字符串是通过手动迭代构建的


0

这为我工作(伪代码):

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

0

我的SQLite和Oracle数据库示例。

第一个For循环用于创建PreparedStatement对象。

第二个For循环用于为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

我的解决方法(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 是包含您的输入/键/字段等的数组

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.