16 StatementHandler:参数绑定、SQL 执行和结果映射的奠基者

StatementHandler 接口是 MyBatis 中非常重要的一个接口,其实现类完成 SQL 语句执行中最核心的一系列操作,这也是后面我们要介绍的 Executor 接口实现的基础。

StatementHandler 接口的定义如下图所示:

Drawing 0.png

StatementHandler 接口中定义的方法

可以看到,其中提供了创建 Statement 对象(prepare() 方法)、为 SQL 语句绑定实参(parameterize() 方法)、执行单条 SQL 语句(query() 方法和 update() 方法)、批量执行 SQL 语句(batch() 方法)等多种功能。

下图展示了 MyBatis 中提供的所有 StatementHandler 接口实现类,以及它们的继承关系:

图片3.png

StatementHandler 接口继承关系图

今天这一讲我们就来详细分析该继承关系图中每个 StatementHandler 实现的核心逻辑。

RoutingStatementHandler

RoutingStatementHandler 这个 StatementHandler 实现,有点策略模式的意味。在 RoutingStatementHandler 的构造方法中,会根据 MappedStatement 中的 statementType 字段值,选择相应的 StatementHandler 实现进行创建,这个新建的 StatementHandler 对象由 RoutingStatementHandler 中的 delegate 字段维护。

RoutingStatementHandler 的构造方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {



// 下面就是根据MappedStatement的配置,生成一个相应的StatementHandler对



// 象,并设置到delegate字段中维护



switch (ms.getStatementType()) {



case STATEMENT:



// 创建SimpleStatementHandler对象



delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);



break;



case PREPARED:



// 创建PreparedStatementHandler对象



delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);



break;



case CALLABLE:



// 创建CallableStatementHandler对象



delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);



break;



default: // 抛出异常



throw new ExecutorException("...");



}



}

在 RoutingStatementHandler 的其他方法中,都会委托给底层的 delegate 对象来完成具体的逻辑

BaseStatementHandler

作为一个抽象类,BaseStatementHandler 只实现了 StatementHandler 接口的 prepare() 方法,其 prepare() 方法实现为新建的 Statement 对象设置了一些参数,例如,timeout、fetchSize 等。BaseStatementHandler 还新增了一个 instantiateStatement() 抽象方法给子类实现,来完成 Statement 对象的其他初始化操作。不过,BaseStatementHandler 中并没有实现 StatementHandler 接口中的数据库操作等核心方法。

了解了 BaseStatementHandler 对 StatementHandler 接口的实现情况之后,我们再来看一下 BaseStatementHandler 的构造方法,其中会初始化执行 SQL 需要的 Executor 对象为 SQL 绑定实参的 ParameterHandler 对象以及生成结果对象的 ResultSetHandler 对象。这三个核心对象中,ResultSetHandler 对象我们已经在[《14 | 探究 MyBatis 结果集映射机制背后的秘密(上)》]中介绍过了,ParameterHandler 和 Executor 在后面会展开介绍。

1. KeyGenerator

这里需要关注的是 generateKeys() 方法,其中会通过 KeyGenerator 接口生成主键,下面我们就来看看 KeyGenerator 接口的相关内容。

我们知道不同数据库的自增 id 生成策略并不完全一样。例如,我们常见的 Oracle DB 是通过sequence 实现自增 id 的,如果使用自增 id 作为主键,就需要我们先获取到这个自增的 id 值,然后再使用;MySQL 在使用自增 id 作为主键的时候,insert 语句中可以不指定主键,在插入过程中由 MySQL 自动生成 id。KeyGenerator 接口支持 insert 语句执行前后获取自增的 id,分别对应 processBefore() 方法和 processAfter() 方法,下图展示了 MyBatis 提供的两个 KeyGenerator 接口实现:

图片4.png

KeyGenerator 接口继承关系图

Jdbc3KeyGenerator 用于获取数据库生成的自增 id(例如 MySQL 那种生成模式),其 processBefore() 方法是空实现,processAfter() 方法会将 insert 语句执行后生成的主键保存到用户传递的实参中。我们在使用 MyBatis 执行单行 insert 语句时,一般传入的实参是一个 POJO 对象或是 Map 对象,生成的主键会设置到对应的属性中;执行多条 insert 语句时,一般传入实参是 POJO 对象集合或 Map 对象的数组或集合,集合中每一个元素都对应一次插入操作,生成的多个自增 id 也会设置到每个元素的相应属性中。

Jdbc3KeyGenerator 中获取数据库自增 id 的核心代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 将数据库生成的自增id作为结果集返回



try (ResultSet rs = stmt.getGeneratedKeys()) {



final ResultSetMetaData rsmd = rs.getMetaData();



final Configuration configuration = ms.getConfiguration();



if (rsmd.getColumnCount() < keyProperties.length) {



} else {



// 处理rs这个结果集,将生成的id设置到对应的属性中



assignKeys(configuration, rs, rsmd, keyProperties, parameter);



}



} catch (Exception e) {



throw new ExecutorException("...");



}

如果使用像 Oracle 这种不支持自动生成主键自增 id 的数据库时,我们可以使用 SelectkeyGenerator 来生成主键 id。Mapper 映射文件中的<selectKey>标签会被解析成 SelectkeyGenerator 对象,其中的 executeBefore 属性(boolean 类型)决定了是在 insert 语句执行之前获取主键,还是在 insert 语句执行之后获取主键 id。

SelectkeyGenerator 中的 processBefore() 方法和 processAfter() 方法都是通过 processGeneratedKeys() 这个私有方法获取主键 id 的,processGeneratedKeys() 方法会执行<selectKey>标签中指定的 select 语句,查询主键信息,并记录到用户传入的实参对象的对应属性中,核心代码片段如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 创建一个新的Executor对象来执行指定的select语句



Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);



// 拿到主键信息



List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);



if (values.size() == 0) {



throw new ExecutorException("SelectKey returned no data.");



} else if (values.size() > 1) {



throw new ExecutorException("SelectKey returned more than one value.");



} else {



// 创建实参对象的MetaObject对象



final MetaObject metaParam = configuration.newMetaObject(parameter);



MetaObject metaResult = configuration.newMetaObject(values.get(0));



if (keyProperties.length == 1) {



// 将主键信息记录到用户传入的实参对象中



if (metaResult.hasGetter(keyProperties[0])) {



setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));



} else {



setValue(metaParam, keyProperties[0], values.get(0));



}



} else {



... // 多结果集的处理



}



}

2. ParameterHandler

介绍完 KeyGenerator 接口之后,我们再来看一下 BaseStatementHandler 中依赖的另一个辅助类—— ParameterHandler。

经过前面[《13 | 深入分析动态 SQL 语句解析全流程(下)》]介绍的一系列 SqlNode 的处理之后,我们得到的 SQL 语句(维护在 BoundSql 对象中)可能包含多个“?”占位符,与此同时,用于替换每个“?”占位符的实参都记录在 BoundSql.parameterMappings 集合中。

ParameterHandler 接口中定义了两个方法:一个是 getParameterObject() 方法,用来获取传入的实参对象;另一个是 setParameters() 方法,用来替换“?”占位符,这是 ParameterHandler 的核心方法

DefaultParameterHandler 是 ParameterHandler 接口的唯一实现,其 setParameters() 方法会遍历 BoundSql.parameterMappings 集合,根据参数名称查找相应实参,最后会通过 PreparedStatement.set*() 方法与 SQL 语句进行绑定。setParameters() 方法的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
for (int i = 0; i < parameterMappings.size(); i++) {



ParameterMapping parameterMapping = parameterMappings.get(i);



Object value;



String propertyName = parameterMapping.getProperty();



// 获取实参值



if (boundSql.hasAdditionalParameter(propertyName)) {



value = boundSql.getAdditionalParameter(propertyName);



} else if (parameterObject == null) {



value = null;



} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {



value = parameterObject;



} else {



MetaObject metaObject = configuration.newMetaObject(parameterObject);



value = metaObject.getValue(propertyName);



}



// 获取TypeHandler



TypeHandler typeHandler = parameterMapping.getTypeHandler();



JdbcType jdbcType = parameterMapping.getJdbcType();



// 底层会调用PreparedStatement.set*()方法完成绑定



typeHandler.setParameter(ps, i + 1, value, jdbcType);



}

SimpleStatementHandler

SimpleStatementHandler 是 StatementHandler 的具体实现之一,继承了 BaseStatementHandler 抽象类。SimpleStatementHandler 各个方法接收的是 java.sql.Statement 对象,并通过该对象来完成 CRUD 操作,所以在 SimpleStatementHandler 中维护的 SQL 语句不能存在“?”占位符,填充占位符的 parameterize() 方法也是空实现。

在 instantiateStatement() 这个初始化方法中,SimpleStatementHandler 会直接通过 JDBC Connection 创建 Statement 对象,这个对象也是后续 SimpleStatementHandler 其他方法的入参。

在 query() 方法实现中,SimpleStatementHandler 会直接通过上面创建的 Statement 对象,执行 SQL 语句,返回的结果集由 ResultSetHandler 完成映射,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {、



// 获取SQL语句



String sql = boundSql.getSql();



// 执行SQL语句



statement.execute(sql);



// 处理ResultSet映射,得到结果对象



return resultSetHandler.handleResultSets(statement);



}

queryCursor() 方法与 query() 方法实现类似,这里就不再赘述。

batch() 方法调用的是 Statement.addBatch() 方法添加批量执行的 SQL 语句,但并不是立即执行,而是等待 Statement.executeBatch() 方法执行时才会批量执行,这点你稍微注意一下即可。

至于 update() 方法,首先会通过 Statement.execute() 方法执行 insert、update 或 delete 类型的 SQL 语句,然后执行 KeyGenerator.processAfter() 方法查询主键并填充相应属性(processBefore() 方法已经在 prepare() 方法中执行过了),最后通过 Statement.getUpdateCount() 方法获取 SQL 语句影响的行数并返回。

PreparedStatementHandler

PreparedStatementHandler 是 StatementHandler 的具体实现之一,也是最常用的 StatementHandler 实现,它同样继承了 BaseStatementHandler 抽象类。PreparedStatementHandler 各个方法接收的是 java.sql.PreparedStatement 对象,并通过该对象来完成 CRUD 操作,在其 parameterize() 方法中会通过前面介绍的 ParameterHandler调用 PreparedStatement.set*() 方法为 SQL 语句绑定参数,所以在 PreparedStatementHandler 中维护的 SQL 语句是可以包含“?”占位符的

在 instantiateStatement() 方法中,PreparedStatementHandler 会直接通过 JDBC Connection 的 prepareStatement() 方法创建 PreparedStatement 对象,该对象就是 PreparedStatementHandler 其他方法的入参。

PreparedStatementHandler 的 query() 方法、batch() 方法以及 update() 方法与 SimpleStatementHandler 的实现基本相同,只不过是把 Statement API 换成了 PrepareStatement API 而已。下面我们以 update() 方法为例进行简单介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public int update(Statement statement) throws SQLException {



PreparedStatement ps = (PreparedStatement) statement;



ps.execute(); // 执行SQL语句,修改数据



int rows = ps.getUpdateCount(); // 获取影响行数



// 获取实参对象



Object parameterObject = boundSql.getParameterObject();



// 执行KeyGenerator



KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();



keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);



return rows; // 返回影响行数



}

CallableStatementHandler

CallableStatementHandler 是处理存储过程的 StatementHandler 实现,其 instantiateStatement() 方法会通过 JDBC Connection 的 prepareCall() 方法为指定存储过程创建对应的 java.sql.CallableStatement 对象。在 parameterize() 方法中,CallableStatementHandler 除了会通过 ParameterHandler 完成实参的绑定之外,还会指定输出参数的位置和类型。

在 CallableStatementHandler 的 query()、queryCursor()、update() 方法中,除了处理 SQL 语句本身的结果集(ResultSet 结果集或是影响行数),还会通过 ResultSetHandler 的 handleOutputParameters() 方法处理输出参数,这是与 PreparedStatementHandler 最大的不同。下面我们以 query() 方法为例进行简单分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {



CallableStatement cs = (CallableStatement) statement;



cs.execute(); // 执行存储过程



// 处理存储过程返回的结果集



List<E> resultList = resultSetHandler.handleResultSets(cs);



// 处理输出参数,可能修改resultList集合



resultSetHandler.handleOutputParameters(cs);



// 返回最后的结果对象



return resultList;



}

总结

这一讲我们重点讲解了 MyBatis 中的 StatementHandler 接口及其核心实现,StatementHandler 接口中定义了执行一条 SQL 语句的核心方法。

  • 首先,分析了 RoutingStatementHandler 实现,它可以帮助我们选择真正的 StatementHandler 实现类。
  • 接下来,介绍了 BaseStatementHandler 这个抽象类的实现,同时还详细阐述了其中使用到的 KeyGenerator 和 ParameterHandler。
  • 最后,又介绍了 SimpleStatementHandler、PreparedStatementHandler 等实现,它们基于 JDBC API 接口,实现了完整的 StatementHandler 功能。