MyBatis3源码深度解析(四)Statement

前言

本节研究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对象
源码1Connection.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结果集的类型。可取值有:

  1. TYPE_FORWARD_ONLY(ResultSet对象的游标只能向前移动)
  2. TYPE_SCROLL_INSENSITIVE(ResultSet对象可滚动,但对数据更改不敏感)
  3. TYPE_SCROLL_SENSITIVE(ResultSet对象可滚动,对数据更改敏感)
  • resultSetConcurrency

ResultSet结果集的并发模式。可取值有:

  1. CONCUR_READ_ONLY(只读)
  2. CONCUR_UPDATABLE(可更改)
  • resultSetHoldability

ResultSet结果集的可持有性。可取值有:

  1. HOLD_CURSORS_OVER_COMMIT(当前事务被提交时不关闭)
  2. CLOSE_CURSORS_AT_COMMIT(当前事务被提交时关闭)
2.4.1.2 执行查询SQL语句
源码2Statement.java

ResultSet executeQuery(String sql) throws SQLException;

由 源码2 可知,使用Statement执行查询SQL语句,调用其executeQuery()方法即可,返回一个结果集ResultSet。

2.4.1.3 执行更新SQL语句

如果SQL语句是一个返回更新数量的DML语句,包括UPDATE、INSERT或者DELETE语句,则需调用executeUpdate()方法。该方法有4个重载的实现:

源码3Statement.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个重载方法。

源码4Statement.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()方法可以获取下一个结果。

源码5Statement.java

boolean getMoreResults() throws SQLException;
boolean getMoreResults(int current) throws SQLException;
  • boolean getMoreResults()

该方法用于获取下一个结果。如果返回值为true时,说明下一个结果为ResulrSet对象;当返回值为false时,说明下一个结果为影响行数,或没有更多结果。

  • boolean getMoreResults(int current)

该方法在getMoreResults()方法的基础上增加了一个current参数,该参数决定了当获取下一个ResultSet对象时,当前ResultSet对象该怎么处理。该参数的取值包括:

  1. Statement.CLOSE_CURRENT_RESULT:当获取下一个ResultSet对象时,当前ResultSet对象应该关闭;
  2. Statement.CLOSE_ALL_RESULTS:当获取下一个ResultSet对象时,当前所有未关闭的ResultSet对象都关闭;
  3. Statement.KEEP_CURRENT_RESULT:当获取下一个ResultSet对象时,当前ResultSet对象不关闭。

如果当前结果是影响行数而不是ResultSet对象,current参数会被忽略。

2.4.1.5 批量执行SQL语句
源码6Statement.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()方法即可。

源码7Connection.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类型的对应关系如图所示:

Java类型和JDBC类型的对应关系

(2)setObject

PreparedStatement接口提供了一个setObject方法,可以将Java类型数据转换为JDBC类型。

源码8PreparedStatement.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个参数,对于不同的数据类型具有不同的含义:

  1. 对于JDBC类型是DECIMAL或NUMERIC类型,该参数的值是指小数点后的位数;
  2. 对于Java类型是InputStream或Reader,该参数是流或Reader中的数据长度;
  3. 对于所有其他类型,该参数将被忽略。
(3)setNull

PreparedStatement接口还提供了一个setNull方法,可以将占位符的值设置JDBC的NULL

源码9PreparedStatement.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对象
源码10Connection.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源码深度解析

  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值