文章目录
前言
本节研究JDBC API中的Statement接口及其子接口PreparedStatement和CallableStatement。
为方便测试验证,从本节开始不再使用MyBatis内置的HSQLDB数据库作为测试数据库,改用MySQL作为测试数据库。
因此在开始学习之前,创建一个MySQL数据库,新建一张表user,并初始化数据:
CREATE DATABASE mybatis_demo;
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
phone VARCHAR(20),
birthday DATETIME
);
insert into user (name, age, phone, birthday) values('user1', 18, '18705464523', '2000-02-21 10:24:30');
insert into user (name, age, phone, birthday) values('user2', 22, '13545684456', '1998-12-20 04:56:12');
再创建一个工具类:
public abstract class DbUtils {
// 创建连接(用户名和密码需换成自己的)
public static Connection getConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_demo",
"xxxx", "******");
}
// 关闭相关资源
public static void close(ResultSet resultSet, Statement statement, Connection connection) {
if(resultSet == null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement == null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection == null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2.4 Statement
2.4.1 java.sql.Statement
Statement接口是JDBC API中操作数据库的核心接口,具体的实现由驱动程序来完成。
2.4.1.1 创建Statement对象
源码1:Connection.java
public interface Connection extends Wrapper, AutoCloseable {
Statement createStatement() throws SQLException;
Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException;
Statement createStatement(int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException;
}
由 源码1 可知,调用Connection对象的createStatement()
方法就可以创建Statement对象。在应用程序中,每个Connection对象可以创建多个Statement对象。
Connection接口提供了几个重载的createStatement()
方法,用于指定ResultSet(结果集)的属性。对参数的解释如下:
- resultSetType
ResultSet结果集的类型。可取值有:
- TYPE_FORWARD_ONLY(ResultSet对象的游标只能向前移动)
- TYPE_SCROLL_INSENSITIVE(ResultSet对象可滚动,但对数据更改不敏感)
- TYPE_SCROLL_SENSITIVE(ResultSet对象可滚动,对数据更改敏感)
- resultSetConcurrency
ResultSet结果集的并发模式。可取值有:
- CONCUR_READ_ONLY(只读)
- CONCUR_UPDATABLE(可更改)
- resultSetHoldability
ResultSet结果集的可持有性。可取值有:
- HOLD_CURSORS_OVER_COMMIT(当前事务被提交时不关闭)
- CLOSE_CURSORS_AT_COMMIT(当前事务被提交时关闭)
2.4.1.2 执行查询SQL语句
源码2:Statement.java
ResultSet executeQuery(String sql) throws SQLException;
由 源码2 可知,使用Statement执行查询SQL语句,调用其executeQuery()
方法即可,返回一个结果集ResultSet。
2.4.1.3 执行更新SQL语句
如果SQL语句是一个返回更新数量的DML语句,包括UPDATE、INSERT或者DELETE语句,则需调用executeUpdate()
方法。该方法有4个重载的实现:
源码3:Statement.java
int executeUpdate(String sql) throws SQLException;
int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException;
int executeUpdate(String sql, int columnIndexes[]) throws SQLException;
int executeUpdate(String sql, String columnNames[]) throws SQLException;
由 源码3 可知,以上4个方法,都是执行一个UPDATE、INSERT或者DELETE语句并返回更新数量;其中后面3个方法使用额外的参数实现一些更高级的功能。
- executeUpdate(String sql, int autoGeneratedKeys)
autoGeneratedKeys参数专门用于INSERT语句,它是一个标志参数,指示自动生成的键是否可用于检索,可选值包括RETURN_GENERATED_KEYS(可检索)、NO_GENERATED_KEYS(不可检索)。
换句话说,当执行INSERT语句向数据库插入一条记录时,如果希望获取到这条记录的自增主键,则需要将autoGeneratedKeys参数设置为RETURN_GENERATED_KEYS(可检索)。
获取自增主键的示例代码如下:
@Test
public void testExecuteUpdate() {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 获取数据库连接
connection = DbUtils.getConnection();
String sql = "insert into user (name, age, phone, birthday) values('猪八戒', 100, '18705464523', '1993-03-05 10:24:30');";
statement = connection.createStatement();
// 设置autoGeneratedKeys参数为RETURN_GENERATED_KEYS
int row = statement.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
System.out.println("本次影响的记录条数:" + row);
resultSet = statement.getGeneratedKeys();
while (resultSet.next()) {
System.out.println("本条记录的自增主键是:" + resultSet.getLong(1));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
DbUtils.close(resultSet, statement, connection);
}
}
控制台输出结果如下:
本次影响的记录条数:1
本条记录的自增主键是:3
- executeUpdate(String sql, int columnIndexes[])
columnIndexes参数也专门用于INSERT语句,它是列的索引组成的数组,表示从插入的行中可以返回的自动生成的列。
该重载方法和上一个有些类似,上一个重载方法默认返回自增主键这一列,但如果一个表中有多列是自动生成的,则该方法则可以通过指定这些自动生成的列的索引来返回多列的值。
换句话说,当执行INSERT语句向数据库插入一条记录时,如果希望获取到这条记录中的某几个自动生成的列的值,则需要传入这些列的索引数组。
在示例中,由于只有第1列id是自动的,获取这列示例代码如下(只展示不一样的部分):
Statement statement = connection.createStatement();
int row = statement.executeUpdate("insert into user (name, age, phone, birthday) values('猪八戒', 100, '18705464523', '1993-03-05 10:24:30');",
new int[]{1});
System.out.println("本次影响的记录条数:" + row);
resultSet = statement.getGeneratedKeys();
while (resultSet.next()) {
System.out.println("本条记录的第1列的值是:" + resultSet.getInt(1));
}
控制台输出结果如下:
本次影响的记录条数:1
本条记录的第1列的值是:4
- executeUpdate(String sql, String columnNames[]):
columnIndexes参数也专门用于INSERT语句,它是列名组成的数组,表示从插入的行中可以返回的自动生成的列。
该重载方法和上一个传入列索引数组的方法效果是一样的,只是把索引值换成了列名。
例如要获取id这一列的值,示例代码如下:
Statement statement = connection.createStatement();
int row = statement.executeUpdate("insert into user (name, age, phone, birthday) values('猪八戒', 100, '18705464523', '1993-03-05 10:24:30');",
new String[]{"id", "age", "birthday"});
System.out.println("本次影响的记录条数:" + row);
resultSet = statement.getGeneratedKeys();
while (resultSet.next()) {
System.out.println("本条记录的id列的值是:" + keys.getInt(1));
}
控制台输出结果如下:
本次影响的记录条数:1
本条记录的id列的值是:5
2.4.1.4 执行不确定类型的SQL语句
在操作数据库时,如果不确定SQL语句的类型,则可以调用excute()
方法,该方法有4个重载方法。
源码4:Statement.java
boolean execute(String sql) throws SQLException;
boolean execute(String sql, int autoGeneratedKeys) throws SQLException;
boolean execute(String sql, int columnIndexes[]) throws SQLException;
boolean execute(String sql, String columnNames[]) throws SQLException;
- execute(String sql)
该方法可以执行一个SQL语句,通过返回值判断SQL类型。
当返回值为true时,说明SQL语句为SELECT语句,可以通过Statement接口的getResultSet()
方法获取查询结果集;
当返回值为false时,说明SQL语句为UPDATE、INSERT或DELETE语句,可以通过Statement接口的getUpdateCount()
方法获取影响的行数。
- execute(String sql, int autoGeneratedKeys)
- execute(String sql, int columnIndexes[])
- execute(String sql, String columnNames[])
以上3个重载方法在上一个的基础上,增加一些额外参数,这些参数的作用与executeUpdate()
方法的作用一致,不再赘述。
需要注意的是,execute()
方法可能会返回多个结果,例如既返回查询结果集,又返回影响行数。通过getMoreResults()
方法可以获取下一个结果。
源码5:Statement.java
boolean getMoreResults() throws SQLException;
boolean getMoreResults(int current) throws SQLException;
- boolean getMoreResults()
该方法用于获取下一个结果。如果返回值为true时,说明下一个结果为ResulrSet对象;当返回值为false时,说明下一个结果为影响行数,或没有更多结果。
- boolean getMoreResults(int current)
该方法在getMoreResults()
方法的基础上增加了一个current参数,该参数决定了当获取下一个ResultSet对象时,当前ResultSet对象该怎么处理。该参数的取值包括:
- Statement.CLOSE_CURRENT_RESULT:当获取下一个ResultSet对象时,当前ResultSet对象应该关闭;
- Statement.CLOSE_ALL_RESULTS:当获取下一个ResultSet对象时,当前所有未关闭的ResultSet对象都关闭;
- Statement.KEEP_CURRENT_RESULT:当获取下一个ResultSet对象时,当前ResultSet对象不关闭。
如果当前结果是影响行数而不是ResultSet对象,current参数会被忽略。
2.4.1.5 批量执行SQL语句
源码6:Statement.java
void addBatch(String sql) throws SQLException;
void clearBatch() throws SQLException;
int[] executeBatch() throws SQLException;
- void addBatch(String sql):把一条SQL语句添加到批量执行的SQL语句列表中(只是加到列表,并未执行);
- void clearBatch():清空批量执行的SQL语句列表;
- int[] executeBatch():批量执行SQL语句列表,返回数组的元素是每条SQL语句的影响条数。
小结:Statement接口除了提供操作数据库的相关方法外,还提供了一系列属性相关的方法,这些方法用于设置或获取Statement相关的属性。
2.4.2 java.sql.PreparedStatement
PreparedStatement接口继承自Statement接口,在Statement接口的基础上增加了参数占位符功能。
2.4.2.1 创建PreparedStatement对象
PreparedStatement对象的实例表示一个可以被预编译的SQL语句,其创建和Statement对象类似,调用Connection接口的prepareStatement()
方法即可。
源码7:Connection.java
PreparedStatement prepareStatement(String sql) throws SQLException;
PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException;
PreparedStatement prepareStatement(String sql, int columnIndexes[]) throws SQLException;
PreparedStatement prepareStatement(String sql, String columnNames[]) throws SQLException;
PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;
PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException;
由 源码7 可知,prepareStatement
方法有6个重载的方法:
- PreparedStatement prepareStatement(String sql)
创建PreparedStatement对象时,就要传入一条SQL语句作为参数,这条SQL语句可以使用“?”作为参数占位符,然后使用PreparedStatement接口提供的setter方法为占位符设置参数。例如:
PreparedStatement statement = connection.prepareStatement("select * from user where id = ? and name = ?");
statement.setLong(1, 1);
statement.setString(2, "User2");
PreparedStatement接口提供的setter方法遵循set<Type>
格式,其中Type为数据类型。它一般有2个参数:第一个参数是指参数占位符的索引(从1开始),即第几个“?”;第二个参数是指为占位符指定的值。
需要注意的是,在使用PreparedStatement对象执行SQL语句之前,必须为每个参数占位符设置对应的值,否则执行SQL语句时会抛出SQLException异常。
- PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
- PreparedStatement prepareStatement(String sql, int columnIndexes[])
- PreparedStatement prepareStatement(String sql, String columnNames[])
这三个重载方法中的额外参数和【2.4.1.3 执行更新SQL语句】中介绍的作用是一样的,不再赘述。
- PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
- PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)
这两个重载方法的额外参数和【2.4.1.1 创建Statement对象】中介绍的作用是一样的,不再赘述。
2.4.2.2 占位符的清除与重设
PreparedStatement对象通过setXXX()
方法设置的参数值在执行后不能被重置,一般需要显式调用clearParameters()
方法清除先前设置的值,再为参数重新设置值。
对于一个给定的PreparedStatement对象,在execute()
、executeQuery()
、executeUpdate()
、executeBatch()
或clearParameters()
方法调用之前,如果占位符已经使用setXXX()
方法设置值,应用程序不可以再次调用setXXX()
方法覆盖已经设置的值。
但是应用程序可以在执行SQL语句的方法或clearParameters()
方法调用之后,再次调用setXXX()
方法覆盖已经设置的值。
2.4.2.3 占位符的数据类型转换
(1)类型转换
在使用setXXX()
方法为参数占位符设置值时存在一个数据转换的过程。setXXX()
方法的参数为Java数据类型,需要转换为JDBC类型(java.sql.Types中定义的SQL类型),这一过程由JDBC驱动来完成。
Java类型和JDBC类型的对应关系如图所示:
(2)setObject
PreparedStatement接口提供了一个setObject
方法,可以将Java类型数据转换为JDBC类型。
源码8:PreparedStatement.java
void setObject(int parameterIndex, Object x) throws SQLException;
void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException;
void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException;
- setObject(int parameterIndex, Object x)
该方法的第1个参数是占位符的索引,第2个参数是任意类型的参数值,JDBC驱动会按照上表中的对应关系进行转换。
- setObject(int parameterIndex, Object x, int targetSqlType)
该方法在上一个的基础上,增加了第3个参数,指定要转换的JDBC类,如果转换失败,则会抛出异常。
- setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength)
该方法在上一个的基础上,增加了第4个参数,对于不同的数据类型具有不同的含义:
- 对于JDBC类型是DECIMAL或NUMERIC类型,该参数的值是指小数点后的位数;
- 对于Java类型是InputStream或Reader,该参数是流或Reader中的数据长度;
- 对于所有其他类型,该参数将被忽略。
(3)setNull
PreparedStatement接口还提供了一个setNull
方法,可以将占位符的值设置JDBC的NULL。
源码9:PreparedStatement.java
void setNull(int parameterIndex, int sqlType) throws SQLException;
void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException;
- setNull(int parameterIndex, int sqlType)
该方法接收两个参数:第1个参数是占位符的索引,第2个参数是JDBC类型。
- setNull(int parameterIndex, int sqlType, String typeName)
该方法在上一个的基础上增加了第3个参数,指定SQL用户自定义类型的全限定类名,即当sqlType参数是STRUCT、JAVA_OBJECT、REF等类型时,typeName需要指定对应的Java对象的全限定类名;否则该参数将被忽略。
2.4.2.4 java.sql.ParameterMetaData
JDBC API提供了一个ParameterMetaData接口,用于描述PreparedStatement对象的参数信息,包括参数个数、参数类型、参数值等。PreparedStatement接口提供了一个getParameterMetaData()
方法获取ParameterMetaData对象实例。例如:
PreparedStatement statement = connection.prepareStatement(
"insert into user (id, name) values(?, ?);");
statement.setInt(1, 12345);
statement.setString(2, "齐天大圣");
ParameterMetaData parameterMetaData = statement.getParameterMetaData();
for (int i = 1; i <= parameterMetaData.getParameterCount(); i++) {
String parameterTypeName = parameterMetaData.getParameterTypeName(i);
String parameterClassName = parameterMetaData.getParameterClassName(i);
System.out.println("第" + i + "个参数,TypeName:" + parameterTypeName + ",ClassName:" + parameterClassName);
}
控制台打印结果:
第1个参数,TypeName:INTEGER,ClassName:java.lang.Integer
第2个参数,TypeName:VARCHAR,ClassName:java.lang.String
在上面的案例中,通过getParameterMetaData()
方法获取PreparedStatement对象相关的ParameterMetaData对象,然后通过getParameterCount()
方法获取参数数量,调用getParameterTypeName()
方法获取参数的JDBC类型,调用getParameterClassName()
方法获取参数的Java类型。
2.4.3 java.sql.CallableStatement
CallableStatement接口继承自PreparedStatement接口,在PreparedStatement接口的基础上增加调用存储过程并检索调用结果的功能。
2.4.3.1 创建CallableStatement对象
源码10:Connection.java
CallableStatement prepareCall(String sql) throws SQLException;
CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;
CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException;
由 源码10 可知,CallableStatement对象是通过Connection接口的prepareCall()
方法创建的,重载方法中的resultSetType、resultSetConcurrency、resultSetHoldability参数和【2.4.1.1 创建Statement对象】中介绍的参数作用是一致的,此处不再赘述。
2.4.3.2 执行无参的存储过程
首先在数据库创建一个无参的存储过程,SQL语句如下:
DELIMITER //
CREATE PROCEDURE SelectAllUsers()
BEGIN
SELECT * FROM user;
END //
测试代码如下:
// ......
String sql = "{ call SelectAllUsers() }";
statement = connection.prepareCall(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setAge(resultSet.getInt("age"));
user.setPhone(resultSet.getString("phone"));
user.setBirthday(resultSet.getDate("birthday"));
System.out.println("获取的用户信息为:" + user);
}
// ......
控制台输出结果:
获取的用户信息为:User{id=1, name='user1', age=18, phone='18705464523', birthday=2000-02-21}
获取的用户信息为:User{id=2, name='user2', age=22, phone='13545684456', birthday=1998-12-20}
在上面的案例中,使用prepareCall()
方法创建CallableStatement对象,传入执行无参存储过程的SQL语句,并调用executeQuery()
方法真正执行,最终返回一个ResultSet对象。
2.4.3.3 执行有参的存储过程
存储过程的参数包括包含IN(输入参数)、OUT(输出参数)、INOUT(输入输出参数)。
要注意,IN或INOUT参数的占位符必须设置值,OUT或INOUT参数的占位符必须通过CallableStatement中的registerOutParameter()
方法先注册。
在数据库创建一个有参的存储过程,SQL语句如下:
DELIMITER //
CREATE PROCEDURE SelectNameByIdWithOut(IN in_id INT, OUT out_name CHAR(50))
BEGIN
SELECT `name` INTO out_name FROM user WHERE id = in_id;
SELECT * FROM user WHERE id = in_id;
END //
测试代码如下:
// ...
String sql = "{ call SelectNameByIdWithOut(?, ?) }";
statement = connection.prepareCall(sql);
// 设置IN参数的值
statement.setInt(1, 1);
// 注册OUT参数
statement.registerOutParameter(2, Types.VARCHAR);
resultSet = statement.executeQuery();
// 获取OUT参数的结果
System.out.println("OUT参数的结果:" + statement.getString(2));
// ...
控制台输出结果:
OUT参数的结果:user1
获取的用户信息为:User{id=1, name='user1', age=18, phone='18705464523', birthday=2000-02-21}
在上面的案例中,调用存储过程SQL语句中有两个参数占位符:第1个是IN参数,需要通过setXXX()
方法设置具体的值;第2个是OUT参数,需要调用registerOutParameter()
方法进行注册,执行后通过getXXX()
方法获取结果。
2.4.3.4 执行返回多个结果集的存储过程
在上面两个示例中,执行存储过程,查询的结果就是一个结果集,但在实际开发中一个存储过程可能有多个查询语句,返回多个结果集。
对于返回多个结果集,则需要借助Statement接口的getMoreResults()
方法来处理。
在数据库创建一个有参的且返回多个结果集存储过程,SQL语句如下:
DELIMITER //
CREATE PROCEDURE SelectUserWithMlutiRs(IN in_id INT, OUT out_name CHAR(50))
BEGIN
SELECT `name` INTO out_name FROM user WHERE id = in_id;
SELECT * FROM user WHERE id = in_id;
SELECT * FROM user;
END //
测试代码如下:
// ...
resultSet = statement.executeQuery();
// 获取OUT参数的结果
System.out.println("OUT参数的结果:" + statement.getString(2));
while (resultSet.next()) {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setAge(resultSet.getInt("age"));
user.setPhone(resultSet.getString("phone"));
user.setBirthday(resultSet.getDate("birthday"));
System.out.println("获取的用户信息为:" + user);
}
System.out.println("-----------");
// 处理下一个结果集
while (statement.getMoreResults()) {
resultSet = statement.getResultSet();
while (resultSet.next()) {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setAge(resultSet.getInt("age"));
user.setPhone(resultSet.getString("phone"));
user.setBirthday(resultSet.getDate("birthday"));
System.out.println("获取的用户信息为:" + user);
}
System.out.println("-----------");
}
// ...
控制台输出结果:
OUT参数的结果:user1
获取的用户信息为:User{id=1, name='user1', age=18, phone='18705464523', birthday=2000-02-21}
-----------
获取的用户信息为:User{id=1, name='user1', age=18, phone='18705464523', birthday=2000-02-21}
获取的用户信息为:User{id=2, name='user2', age=22, phone='13545684456', birthday=1998-12-20}
-----------
在上面的示例中,在处理完第一个ResultSet结果集后,借助Statement接口的getMoreResults()
方法来判断是否还有下一个结果集,如果有则调用statement.getResultSet()
方法获取下一个结果集,并进行处理。
…
本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析