Java PreparedStatement IN子句替代
如果您使用JDBC API在数据库上运行查询,则应该知道PreparedStatement比Statement是更好的选择。
但是,由于JDBC API仅对"?"使用一个文字。
参数,PreparedStatement不适用于IN子句查询。
PreparedStatement IN子句
因此,如果需要使用IN子句执行数据库查询,则需要寻找一些替代方法。
这篇文章的目的是分析不同的方法,您可以选择适合您需求的方法。
- 执行单一查询
- 使用存储过程
- 动态创建PreparedStatement查询
- 在PreparedStatement查询中使用NULL
让我们一一看一下这些方法。
但是在此之前,我们先创建一个实用程序,用于从属性文件读取数据库连接。
db.properties
#mysql DB properties DB_DRIVER_CLASS=com.mysql.jdbc.Driver DB_URL=jdbc:mysql://localhost:3306/UserDB DB_USERNAME=hyman DB_PASSWORD=hyman123 #Oracle DB Properties #DB_DRIVER_CLASS=oracle.jdbc.driver.OracleDriver #DB_URL=jdbc:oracle:thin:@localhost:1521:orcl #DB_USERNAME=hr #DB_PASSWORD=oracle
package com.theitroad.jdbc.preparedstatement.in; import java.io.FileInputStream; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBConnection { public static Connection getConnection() { Properties props = new Properties(); FileInputStream fis = null; Connection con = null; try { fis = new FileInputStream("db.properties"); props.load(fis); //load the Driver Class Class.forName(props.getProperty("DB_DRIVER_CLASS")); //create the connection now con = DriverManager.getConnection(props.getProperty("DB_URL"), props.getProperty("DB_USERNAME"), props.getProperty("DB_PASSWORD")); } catch (SQLException e) { System.out.println("Check database is UP and configs are correct"); e.printStackTrace(); } catch (IOException e) { System.out.println("Looks like db.property file has some issues"); e.printStackTrace(); } catch (ClassNotFoundException e) { System.out.println("Please include JDBC API jar in classpath"); e.printStackTrace(); }finally{ try { fis.close(); } catch (IOException e) { System.out.println("File Close issue, lets ignore it."); } } return con; } }
确保项目的构建路径中有JDBC jar。
现在,让我们看一下不同的方法及其分析。
执行单一查询
这是最简单的方法。
我们可以获取输入并多次执行单个PreparedStatement查询。
使用这种方法的示例程序如下所示。
JDBCPreparedStatementSingle.java
package com.theitroad.jdbc.preparedstatement.in; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class JDBCPreparedStatementSingle { private static final String QUERY = "select empid, name from Employee where empid = ?"; public static void printData(int[] ids){ Connection con = DBConnection.getConnection(); PreparedStatement ps = null; ResultSet rs = null; try { ps = con.prepareStatement(QUERY); for(int empid : ids){ ps.setInt(1, empid); rs = ps.executeQuery(); while(rs.next()){ System.out.println("Employee ID="+rs.getInt("empid")+", Name="+rs.getString("name")); } //close the resultset here try{ rs.close(); } catch(SQLException e){} } } catch (SQLException e) { e.printStackTrace(); }finally{ try { ps.close(); con.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
这种方法很简单,但是非常慢,因为如果有100个参数,那么它将进行100次数据库调用。
这将导致100个ResultSet对象,这些对象将使系统超载,并且也将导致性能下降。
因此,不建议使用此方法。
使用存储过程
我们可以编写一个存储过程,并将输入数据发送到该存储过程。
然后,我们可以在存储过程中一一执行查询并获得结果。
这种方法提供了最快的性能,但是众所周知,存储过程是特定于数据库的。
因此,如果我们的应用程序处理多种类型的数据库,例如Oracle,MySQL,则将很难维护。
仅在处理单一类型的数据库并且没有计划更改数据库服务器时,才应使用此方法。
由于编写存储过程超出了本教程的范围,因此我将不演示如何使用它。
动态创建PreparedStatement查询
这种方法涉及编写逻辑以根据IN子句中元素的大小动态创建PreparedStatement查询。
一个显示如何使用它的简单示例如下代码所示。
JDBCPreparedStatementDynamic.java
package com.theitroad.jdbc.preparedstatement.in; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class JDBCPreparedStatementDynamic { public static void printData(int[] ids){ String query = createQuery(ids.length); System.out.println("Query="+query); Connection con = DBConnection.getConnection(); PreparedStatement ps = null; ResultSet rs = null; try { ps = con.prepareStatement(query); for(int i = 1; i <=ids.length; i++){ ps.setInt(i, ids[i-1]); } rs = ps.executeQuery(); while(rs.next()){ System.out.println("Employee ID="+rs.getInt("empid")+", Name="+rs.getString("name")); } //close the resultset here try{ rs.close(); } catch(SQLException e){} } catch (SQLException e) { e.printStackTrace(); }finally{ try { ps.close(); con.close(); } catch (SQLException e) { e.printStackTrace(); } } } private static String createQuery(int length) { String query = "select empid, name from Employee where empid in ("; StringBuilder queryBuilder = new StringBuilder(query); for( int i = 0; i< length; i++){ queryBuilder.append(" ?"); if(i != length -1) queryBuilder.append(","); } queryBuilder.append(")"); return queryBuilder.toString(); } }
请注意,查询是动态创建的,它将完美运行。
只有一个数据库调用,并且性能会很好。
但是,如果用户输入的大小相差很大,我们将无法获得PreparedStatement的优势来缓存和重用执行计划。
如果您不担心PreparedStatement缓存并且使用IN子句的查询不多,那么这似乎是可行的方法。
在PreparedStatement查询中使用NULL
如果您真的想利用PreparedStatement缓存功能,那么另一种方法是在PreparedStatement参数中使用NULL。
假设查询中允许的最大参数为10,那么我们可以像下面这样编写逻辑。
JDBCPreparedStatementNULL.java
package com.theitroad.jdbc.preparedstatement.in; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class JDBCPreparedStatementNULL { private static final String QUERY = "select empid, name from Employee where empid in ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; private static final int PA内存_SIZE = 10; public static void printData(int[] ids){ if(ids.length > PA内存_SIZE){ System.out.println("Maximum input size supported is "+PA内存_SIZE); //in real life, we can write logic to execute in batches, for simplicity I am returning return; } Connection con = DBConnection.getConnection(); PreparedStatement ps = null; ResultSet rs = null; try { ps = con.prepareStatement(QUERY); int i = 1; for(; i <=ids.length; i++){ ps.setInt(i, ids[i-1]); } //set null for remaining ones for(; i<=PA内存_SIZE;i++){ ps.setNull(i, java.sql.Types.INTEGER); } rs = ps.executeQuery(); while(rs.next()){ System.out.println("Employee ID="+rs.getInt("empid")+", Name="+rs.getString("name")); } //close the resultset here try{ rs.close(); } catch(SQLException e){} } catch (SQLException e) { e.printStackTrace(); }finally{ try { ps.close(); con.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
注意,上面的程序使用相同的PreparedStatement查询来执行IN子句语句,并将获得查询缓存和执行计划的好处。
为了简单起见,我只是返回输入参数的数量是否大于查询中的参数大小,但是我们可以轻松地扩展它以批量执行以允许任意数量的输入。
现在,让我们编写一个简单的测试程序来检查输出。
对于我的测试程序,我使用在JDBC DataSource示例中创建的Employee表。
我们的测试程序代码是;
JDBCPreparedStatementINTest.java
package com.theitroad.jdbc.preparedstatement.in; public class JDBCPreparedStatementINTest { private static int[] ids = {1,2,3,4,5,6,7,8,9,10}; public static void main(String[] args) { JDBCPreparedStatementSingle.printData(ids); System.out.println("*"); JDBCPreparedStatementDynamic.printData(ids); System.out.println("*"); JDBCPreparedStatementNULL.printData(new int[]{1,2,3,4,5}); } }
当我们用Employee表中的一些测试数据执行它时,我们得到下面的输出。
Employee ID=1, Name=hyman Employee ID=2, Name=David Employee ID=3, Name=Ram Employee ID=4, Name=Leela Employee ID=5, Name=Lisa Employee ID=6, Name=Saurabh Employee ID=7, Name=Mani Employee ID=8, Name=Avinash Employee ID=9, Name=Vijay * Query=select empid, name from Employee where empid in ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) Employee ID=1, Name=hyman Employee ID=2, Name=David Employee ID=3, Name=Ram Employee ID=4, Name=Leela Employee ID=5, Name=Lisa Employee ID=6, Name=Saurabh Employee ID=7, Name=Mani Employee ID=8, Name=Avinash Employee ID=9, Name=Vijay * Employee ID=1, Name=hyman Employee ID=2, Name=David Employee ID=3, Name=Ram Employee ID=4, Name=Leela Employee ID=5, Name=Lisa