Java Callable接口详解:使用示例与最佳实践

Java中的CallableStatement用于从Java程序中调用存储过程。存储过程是一组在数据库中编译的语句,用于执行某些任务。当我们处理多个包含复杂情况的表时,存储过程非常有用。与向数据库发送多个查询不同,我们可以将所需数据发送到存储过程,并在数据库服务器本身上执行逻辑。

可调用语句JDBC API通过CallableStatement接口提供执行存储过程的支持。存储过程必须使用特定于数据库的语法编写,在本教程中,我将使用Oracle数据库。我们将研究CallableStatement的标准特性,包括输入和输出参数。然后,我们将讨论Oracle特定的STRUCT和Cursor示例。让我们首先使用下面的SQL查询在我们的CallableStatement示例程序中创建一个表create_employee.sql。

-- For Oracle DB
CREATE TABLE EMPLOYEE
  (
    "EMPID"   NUMBER NOT NULL ENABLE,
    "NAME"    VARCHAR2(10 BYTE) DEFAULT NULL,
    "ROLE"    VARCHAR2(10 BYTE) DEFAULT NULL,
    "CITY"    VARCHAR2(10 BYTE) DEFAULT NULL,
    "COUNTRY" VARCHAR2(10 BYTE) DEFAULT NULL,
    PRIMARY KEY ("EMPID")
  );

让我们首先创建一个实用类来获取Oracle数据库的Connection对象。确保Oracle OJDBC jar文件在项目的构建路径中。DBConnection.java。

package com.Olivia.jdbc.storedproc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBConnection {

	private static final String DB_DRIVER_CLASS = "oracle.jdbc.driver.OracleDriver";
	private static final String DB_URL = "jdbc:oracle:thin:@localhost:1521:orcl";
	private static final String DB_USERNAME = "HR";
	private static final String DB_PASSWORD = "oracle";
	
	public static Connection getConnection() {
		Connection con = null;
		try {
			// load the Driver Class
			Class.forName(DB_DRIVER_CLASS);

			// create the connection now
			con = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return con;
	}
}

可调用语句示例我们来编写一个简单的存储过程,向Employee表中插入数据。insertEmployee.sql

CREATE OR REPLACE PROCEDURE insertEmployee
(in_id IN EMPLOYEE.EMPID%TYPE,
 in_name IN EMPLOYEE.NAME%TYPE,
 in_role IN EMPLOYEE.ROLE%TYPE,
 in_city IN EMPLOYEE.CITY%TYPE,
 in_country IN EMPLOYEE.COUNTRY%TYPE,
 out_result OUT VARCHAR2)
AS
BEGIN
  INSERT INTO EMPLOYEE (EMPID, NAME, ROLE, CITY, COUNTRY) 
  values (in_id,in_name,in_role,in_city,in_country);
  commit;
  
  out_result := 'TRUE';
  
EXCEPTION
  WHEN OTHERS THEN 
  out_result := 'FALSE';
  ROLLBACK;
END;

如您所见,insertEmployee过程预期从调用者那里接收输入,并将其插入到Employee表中。如果插入语句成功执行,则返回TRUE;而如果出现任何异常,则返回FALSE。让我们看看如何使用CallableStatement来执行insertEmployee存储过程以插入员工数据。JDBCStoredProcedureWrite.java

package com.Olivia.jdbc.storedproc;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Scanner;

public class JDBCStoredProcedureWrite {

	public static void main(String[] args) {
		Connection con = null;
		CallableStatement stmt = null;
		
		//Read User Inputs
		Scanner input = new Scanner(System.in);
		System.out.println("Enter Employee ID (int):");
		int id = Integer.parseInt(input.nextLine());
		System.out.println("Enter Employee Name:");
		String name = input.nextLine();
		System.out.println("Enter Employee Role:");
		String role = input.nextLine();
		System.out.println("Enter Employee City:");
		String city = input.nextLine();
		System.out.println("Enter Employee Country:");
		String country = input.nextLine();
		
		try{
			con = DBConnection.getConnection();
			stmt = con.prepareCall("{call insertEmployee(?,?,?,?,?,?)}");
			stmt.setInt(1, id);
			stmt.setString(2, name);
			stmt.setString(3, role);
			stmt.setString(4, city);
			stmt.setString(5, country);
			
			//register the OUT parameter before calling the stored procedure
			stmt.registerOutParameter(6, java.sql.Types.VARCHAR);
			
			stmt.executeUpdate();
			
			//read the OUT parameter now
			String result = stmt.getString(6);
			
			System.out.println("Employee Record Save Success::"+result);
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try {
				stmt.close();
				con.close();
				input.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

}

我们正在读取用户输入并将其存储在员工表中。与PreparedStatement唯一不同的是通过”{call insertEmployee(?,?,?,?,?,?)}”创建CallableStatement,以及使用CallableStatement的registerOutParameter()方法设置OUT参数。在执行存储过程之前,我们必须注册OUT参数。一旦执行存储过程,我们就可以使用CallableStatement的getXXX()方法获取OUT对象数据。请注意,在注册OUT参数时,我们需要通过java.sql.Types指定OUT参数的类型。代码的性质是通用的,因此如果我们在其他关系型数据库(如MySQL)中有同样的存储过程,我们也可以使用这个程序来执行它们。当我们多次执行上述CallableStatement示例程序时,下面是输出结果。

Enter Employee ID (int):
1
Enter Employee Name:
Pankaj
Enter Employee Role:
Developer
Enter Employee City:
Bangalore
Enter Employee Country:
India
Employee Record Save Success::TRUE

-----
Enter Employee ID (int):
2
Enter Employee Name:
Pankaj Kumar
Enter Employee Role:
CEO
Enter Employee City:
San Jose
Enter Employee Country:
USA
Employee Record Save Success::FALSE

注意到第二次执行失败是因为传递的名称大于列的大小。我们在存储过程中捕获了异常并返回false。

可调用语句示例 – 存储过程的 OUT 参数

使用CallableStatement调用存储过程获取员工数据

现在让我们编写一个存储过程来通过ID获取员工数据。用户将输入员工ID,程序将显示员工信息。使用getEmployee.sql。

create or replace
PROCEDURE getEmployee
(in_id IN EMPLOYEE.EMPID%TYPE,
 out_name OUT EMPLOYEE.NAME%TYPE,
 out_role OUT EMPLOYEE.ROLE%TYPE,
 out_city OUT EMPLOYEE.CITY%TYPE,
 out_country OUT EMPLOYEE.COUNTRY%TYPE
 )
AS
BEGIN
  SELECT NAME, ROLE, CITY, COUNTRY 
  INTO out_name, out_role, out_city, out_country
  FROM EMPLOYEE
  WHERE EMPID = in_id;
  
END;

使用getEmployee存储过程读取员工数据的Java CallableStatement示例程序是JDBCStoredProcedureRead.java。

package com.Olivia.jdbc.storedproc;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Scanner;

public class JDBCStoredProcedureRead {

	public static void main(String[] args) {
		Connection con = null;
		CallableStatement stmt = null;
		
		//读取用户输入
		Scanner input = new Scanner(System.in);
		System.out.println("请输入员工ID(整数):");
		int id = Integer.parseInt(input.nextLine());
		
		try{
			con = DBConnection.getConnection();
			stmt = con.prepareCall("{call getEmployee(?,?,?,?,?)}");
			stmt.setInt(1, id);
			
			//在调用存储过程之前注册OUT参数
			stmt.registerOutParameter(2, java.sql.Types.VARCHAR);
			stmt.registerOutParameter(3, java.sql.Types.VARCHAR);
			stmt.registerOutParameter(4, java.sql.Types.VARCHAR);
			stmt.registerOutParameter(5, java.sql.Types.VARCHAR);
			
			stmt.execute();
			
			//现在读取OUT参数
			String name = stmt.getString(2);
			String role = stmt.getString(3);
			String city = stmt.getString(4);
			String country = stmt.getString(5);
			
			if(name !=null){
			System.out.println("员工姓名="+name+",职位="+role+",城市="+city+",国家="+country);
			}else{
				System.out.println("未找到ID为"+id+"的员工");
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try {
				stmt.close();
				con.close();
				input.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

}

再说一次,这个程序是通用的,并且适用于具有相同存储过程的任何数据库。让我们看看当我们执行上述CallableStatement示例程序时的输出是什么。

请输入员工ID(整数):
1
员工姓名=Pankaj,职位=Developer,城市=Bangalore,国家=India

CallableStatement示例 – 使用Oracle游标的存储过程

由于我们是通过ID来读取员工信息,所以我们得到的是单个结果,OUT参数很适用于读取数据。但是如果我们通过角色或国家来搜索,可能会得到多行结果,在这种情况下,我们可以使用Oracle的游标(CURSOR)来像结果集一样读取它们。使用getEmployeeByRole.sql语句。

create or replace
PROCEDURE getEmployeeByRole
(in_role IN EMPLOYEE.ROLE%TYPE,
 out_cursor_emps OUT SYS_REFCURSOR
 )
AS
BEGIN
  OPEN out_cursor_emps FOR
  SELECT EMPID, NAME, CITY, COUNTRY 
  FROM EMPLOYEE
  WHERE ROLE = in_role;
  
END;

JDBCStoredProcedureCursor.java

package com.Olivia.jdbc.storedproc;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;

import oracle.jdbc.OracleTypes;

public class JDBCStoredProcedureCursor {

	public static void main(String[] args) {

		Connection con = null;
		CallableStatement stmt = null;
		ResultSet rs = null;
		
		//读取用户输入
		Scanner input = new Scanner(System.in);
		System.out.println("请输入员工职位:");
		String role = input.nextLine();
		
		try{
			con = DBConnection.getConnection();
			stmt = con.prepareCall("{call getEmployeeByRole(?,?)}");
			stmt.setString(1, role);
			
			//在调用存储过程之前注册OUT参数
			stmt.registerOutParameter(2, OracleTypes.CURSOR);
			
			stmt.execute();
			
			//现在读取OUT参数
			rs = (ResultSet) stmt.getObject(2);
			
			while(rs.next()){
				System.out.println("员工ID="+rs.getInt("empId")+",姓名="+rs.getString("name")+
						",职位="+role+",城市="+rs.getString("city")+
						",国家="+rs.getString("country"));
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try {
				rs.close();
				stmt.close();
				con.close();
				input.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

}

这个程序使用了Oracle OJDBC的特定类,不能与其他数据库一起使用。我们将OUT参数类型设置为OracleTypes.CURSOR,然后将其转换为ResultSet对象。代码的其他部分是简单的JDBC编程。当我们执行上述CallableStatement示例程序时,我们将得到以下输出。

请输入员工职位:
Developer
员工ID=5,姓名=Kumar,职位=Developer,城市=San Jose,国家=USA
员工ID=1,姓名=Pankaj,职位=Developer,城市=Bangalore,国家=India

根据您的员工表中的数据,您的输出可能会有所不同。

CallableStatement示例 – Oracle数据库对象和STRUCT

这是《Java中CallableStatement使用示例》系列文章的第3部分(共3部分)。

在查看insertEmployee和getEmployee存储过程时,你会发现我在这些过程中使用了Employee表的所有参数。当列数增加时,这种方式可能会引起混淆并且更容易出错。Oracle数据库提供了创建数据库对象的选项,我们可以使用Oracle STRUCT来操作这些对象。让我们首先为Employee表列定义Oracle数据库对象。请执行EMPLOYEE_OBJ.sql文件。

create or replace TYPE EMPLOYEE_OBJ AS OBJECT
(
  EMPID NUMBER,
  NAME VARCHAR2(10),
  ROLE VARCHAR2(10),
  CITY  VARCHAR2(10),
  COUNTRY  VARCHAR2(10)
  
  );

现在让我们使用EMPLOYEE_OBJ来重写insertEmployee存储过程。请参考insertEmployeeObject.sql文件。

CREATE OR REPLACE PROCEDURE insertEmployeeObject
(IN_EMPLOYEE_OBJ IN EMPLOYEE_OBJ,
 out_result OUT VARCHAR2)
AS
BEGIN
  INSERT INTO EMPLOYEE (EMPID, NAME, ROLE, CITY, COUNTRY) values 
  (IN_EMPLOYEE_OBJ.EMPID, IN_EMPLOYEE_OBJ.NAME, IN_EMPLOYEE_OBJ.ROLE, IN_EMPLOYEE_OBJ.CITY, IN_EMPLOYEE_OBJ.COUNTRY);
  commit;
  
  out_result := 'TRUE';
  
EXCEPTION
  WHEN OTHERS THEN 
  out_result := 'FALSE';
  ROLLBACK;
END;

让我们看看如何在Java程序中调用insertEmployeeObject存储过程。请参考JDBCStoredProcedureOracleStruct.java文件。

package com.Olivia.jdbc.storedproc;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Scanner;

import oracle.jdbc.OracleCallableStatement;
import oracle.sql.STRUCT;
import oracle.sql.StructDescriptor;

public class JDBCStoredProcedureOracleStruct {

	public static void main(String[] args) {
		Connection con = null;
		OracleCallableStatement stmt = null;
		
		//Create Object Array for Stored Procedure call
		Object[] empObjArray = new Object[5];
		//Read User Inputs
		Scanner input = new Scanner(System.in);
		System.out.println("Enter Employee ID (int):");
		empObjArray[0] = Integer.parseInt(input.nextLine());
		System.out.println("Enter Employee Name:");
		empObjArray[1] = input.nextLine();
		System.out.println("Enter Employee Role:");
		empObjArray[2] = input.nextLine();
		System.out.println("Enter Employee City:");
		empObjArray[3] = input.nextLine();
		System.out.println("Enter Employee Country:");
		empObjArray[4] = input.nextLine();
		
		try{
			con = DBConnection.getConnection();
			
			StructDescriptor empStructDesc = StructDescriptor.createDescriptor("EMPLOYEE_OBJ", con);
			STRUCT empStruct = new STRUCT(empStructDesc, con, empObjArray);
			stmt = (OracleCallableStatement) con.prepareCall("{call insertEmployeeObject(?,?)}");
			
			stmt.setSTRUCT(1, empStruct);
			
			//register the OUT parameter before calling the stored procedure
			stmt.registerOutParameter(2, java.sql.Types.VARCHAR);
			
			stmt.executeUpdate();
			
			//read the OUT parameter now
			String result = stmt.getString(2);
			
			System.out.println("Employee Record Save Success::"+result);
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try {
				stmt.close();
				con.close();
				input.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

}

首先,我们创建一个与EMPLOYEE_OBJ数据库对象长度相同的对象数组。然后,我们根据EMPLOYEE_OBJ对象的变量顺序设置值。这一点非常重要,否则数据可能会插入到错误的列中。接着,我们使用oracle.sql.StructDescriptor和我们的对象数组创建oracle.sql.STRUCT对象。一旦STRUCT对象被创建,我们将其设置为存储过程的输入参数,注册输出参数并执行存储过程。需要注意的是,这段代码与OJDBC API紧密耦合,不适用于其他数据库。执行此程序时,输出结果如下:

Enter Employee ID (int):
5
Enter Employee Name:
Kumar
Enter Employee Role:
Developer
Enter Employee City:
San Jose
Enter Employee Country:
USA
Employee Record Save Success::TRUE

我们还可以将数据库对象作为OUT参数使用,并从数据库中读取相应的值。以上就是关于在Java中使用CallableStatement执行存储过程的示例,希望这些内容对你有所帮助。

bannerAds