Java PreparedStatement IN子句替代

时间:2020-02-23 14:36:45  来源:igfitidea点击:

如果您使用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