code-learning/mybatis/23-mybatis-SQL 执行(三)之 KeyGenerator.md

601 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 精尽 MyBatis 源码分析 —— SQL 执行(三)之 KeyGenerator
# 1. 概述
本文,我们来分享 SQL 执行的第三部分,`keygen` 包。整体类图如下:[![类图](23-mybatis-SQL 执行(三)之 KeyGenerator.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_03_06/01.png)类图
- 我们可以看到,整体是以 KeyGenerator 为核心。所以,本文主要会看到的就是 KeyGenerator 对**自增主键**的获取。
# 2. KeyGenerator
`org.apache.ibatis.executor.keygen.KeyGenerator` ,主键生成器接口。代码如下:
```
// KeyGenerator.java
public interface KeyGenerator {
// SQL 执行前
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
// SQL 执行后
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
```
- 可在 SQL 执行**之前**或**之后**,进行处理主键的生成。
- 实际上KeyGenerator 类的命名虽然包含 Generator ,但是目前 MyBatis 默认的 KeyGenerator 实现类,都是基于数据库来实现**主键自增**的功能。
- `parameter` 参数,指的是什么呢?以下面的方法为示例:
```
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert({"insert into country (countryname,countrycode) values (#{countryname},#{countrycode})"})
int insertBean(Country country);
```
- 上面的,`country` 方法参数,就是一个 `parameter` 参数。
- **KeyGenerator 在获取到主键后,会设置回 `parameter` 参数的对应属性**。
KeyGenerator 有三个子类,如下图所示:[![类图](23-mybatis-SQL 执行(三)之 KeyGenerator.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_03_06/01.png)类图
- 具体的,我们下面逐小节来分享。
# 3. Jdbc3KeyGenerator
`org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator` ,实现 KeyGenerator 接口,基于 `Statement#getGeneratedKeys()` 方法的 KeyGenerator 实现类,适用于 MySQL、H2 主键生成。
## 3.1 构造方法
```
// Jdbc3KeyGenerator.java
/**
* A shared instance.
*
* 共享的单例
*
* @since 3.4.3
*/
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
```
- 单例。
## 3.2 processBefore
```
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// do nothing
}
```
- 空实现。因为对于 Jdbc3KeyGenerator 类的主键,是在 SQL 执行后,才生成。
## 3.3 processAfter
```
// Jdbc3KeyGenerator.java
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, parameter);
}
```
- 调用 `#processBatch(Executor executor, MappedStatement ms, Statement stmt, Object parameter)` 方法,处理返回的自增主键。单个 `parameter` 参数,可以认为是批量的一个**特例**。
## 3.4 processBatch
```
// Jdbc3KeyGenerator.java
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
// <1> 获得主键属性的配置。如果为空,则直接返回,说明不需要主键
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
ResultSet rs = null;
try {
// <2> 获得返回的自增主键
rs = stmt.getGeneratedKeys();
final Configuration configuration = ms.getConfiguration();
if (rs.getMetaData().getColumnCount() >= keyProperties.length) {
// <3> 获得唯一的参数对象
Object soleParam = getSoleParameter(parameter);
if (soleParam != null) {
// <3.1> 设置主键们,到参数 soleParam 中
assignKeysToParam(configuration, rs, keyProperties, soleParam);
} else {
// <3.2> 设置主键们,到参数 parameter 中
assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter);
}
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
} finally {
// <4> 关闭 ResultSet 对象
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
// ignore
}
}
}
}
```
- `<1>` 处,获得主键属性的配置。如果为空,则直接返回,说明不需要主键。
- 【重要】`<2>` 处,调用 `Statement#getGeneratedKeys()` 方法,获得返回的自增主键。
- ```
<3>
```
处,调用
```
#getSoleParameter(Object parameter)
```
方法,获得唯一的参数对象。详细解析,先跳到
「3.4.1 getSoleParameter」
- `<3.1>` 处,调用 `#assignKeysToParam(...)` 方法,设置主键们,到参数 `soleParam` 中。详细解析,见 [「3.4.2 assignKeysToParam」](https://svip.iocoder.cn/MyBatis/executor-3/#) 。
- `<3.2>` 处,调用 `#assignKeysToOneOfParams(...)` 方法,设置主键们,到参数 `parameter` 中。详细解析,见 [「3.4.3 assignKeysToOneOfParams」](https://svip.iocoder.cn/MyBatis/executor-3/#) 。
- `<4>` 处,关闭 ResultSet 对象。
### 3.4.1 getSoleParameter
```
// Jdbc3KeyGenerator.java
/**
* 获得唯一的参数对象
*
* 如果获得不到唯一的参数对象,则返回 null
*
* @param parameter 参数对象
* @return 唯一的参数对象
*/
private Object getSoleParameter(Object parameter) {
// <1> 如果非 Map 对象,则直接返回 parameter
if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) {
return parameter;
}
// <3> 如果是 Map 对象,则获取第一个元素的值
// <2> 如果有多个元素,则说明获取不到唯一的参数对象,则返回 null
Object soleParam = null;
for (Object paramValue : ((Map<?, ?>) parameter).values()) {
if (soleParam == null) {
soleParam = paramValue;
} else if (soleParam != paramValue) {
soleParam = null;
break;
}
}
return soleParam;
}
```
- `<1>` 处,如下可以符合这个条件。代码如下:
```
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert({"insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})"})
int insertNamedBean(@Param("country") Country country);
```
- `<2>` 处,如下可以符合这个条件。代码如下:
```
@Options(useGeneratedKeys = true, keyProperty = "country.id")
@Insert({"insert into country (countryname, countrycode) values (#{country.countryname}, #{country.countrycode})"})
int insertMultiParams_keyPropertyWithWrongParamName2(@Param("country") Country country,
@Param("someId") Integer someId);
```
- 虽然有 `country` 和 `someId` 参数,但是最终会被封装成一个 `parameter` 参数,类型为 ParamMap 类型。为什么呢?答案在 `ParamNameResolver#getNamedParams(Object[] args)` 方法中。
- 如果是这个情况,获得的主键,会设置回 `country` 的 `id` 属性,因为注解上的 `keyProperty = "country.id"` 配置。
- 😈 此处比较绕,也相对用的少。
- `<3>` 处,如下可以符合这个条件。代码如下:
```
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert({"insert into country (countryname, countrycode) values (#{country.countryname}, #{country.countrycode})"})
int insertMultiParams_keyPropertyWithWrongParamName3(@Param("country") Country country);
```
- 相比 `<2>` 的示例,主要是 `keyProperty = "id"` 的修改,和去掉了 `@Param("someId") Integer someId` 参数。
- 实际上,这种情况,和 `<1>` 是类似的。
- 三种情况,`<2>` 和 `<3>` 有点复杂,胖友实际上,理解 `<1>` 即可。
### 3.4.2 assignKeysToParam
```
// Jdbc3KeyGenerator.java
private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties, Object param)
throws SQLException {
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
final ResultSetMetaData rsmd = rs.getMetaData();
// Wrap the parameter in Collection to normalize the logic.
// <1> 包装成 Collection 对象
Collection<?> paramAsCollection;
if (param instanceof Object[]) {
paramAsCollection = Arrays.asList((Object[]) param);
} else if (!(param instanceof Collection)) {
paramAsCollection = Collections.singletonList(param);
} else {
paramAsCollection = (Collection<?>) param;
}
TypeHandler<?>[] typeHandlers = null;
// <2> 遍历 paramAsCollection 数组
for (Object obj : paramAsCollection) {
// <2.1> 顺序遍历 rs
if (!rs.next()) {
break;
}
// <2.2> 创建 MetaObject 对象
MetaObject metaParam = configuration.newMetaObject(obj);
// <2.3> 获得 TypeHandler 数组
if (typeHandlers == null) {
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
}
// <2.4> 填充主键们
populateKeys(rs, metaParam, keyProperties, typeHandlers);
}
}
```
- `<1>` 处,包装成 Collection 对象。通过这样的方式,使单个 `param` 参数的情况下,可以统一。
- `<2>` 处,遍历 `paramAsCollection` 数组:
- `<2.1>` 处, 顺序遍历 `rs` ,相当于把当前的 ResultSet 对象的主键们,赋值给 `obj` 对象的对应属性。
- `<2.2>` 处,创建 MetaObject 对象,实现对 `obj` 对象的属性访问。
- `<2.3>` 处,调用 `#getTypeHandlers(...)` 方法,获得 TypeHandler 数组。代码如下:
```
// Jdbc3KeyGenerator.java
private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
// 获得主键们,对应的每个属性的,对应的 TypeHandler 对象
TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
for (int i = 0; i < keyProperties.length; i++) {
if (metaParam.hasSetter(keyProperties[i])) {
Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
typeHandlers[i] = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
} else {
throw new ExecutorException("No setter found for the keyProperty '" + keyProperties[i] + "' in '"
+ metaParam.getOriginalObject().getClass().getName() + "'.");
}
}
return typeHandlers;
}
```
- x
- `<2.4>` 处,调用 `#populateKeys(...)` 方法,填充主键们。详细解析,见 [「3.5 populateKeys」](https://svip.iocoder.cn/MyBatis/executor-3/#) 。
### 3.4.3 assignKeysToOneOfParams
```
// Jdbc3KeyGenerator.java
protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties,
Map<?, ?> paramMap) throws SQLException {
// Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'.
// <1> 需要有 `.` 。
int firstDot = keyProperties[0].indexOf('.');
if (firstDot == -1) {
throw new ExecutorException(
"Could not determine which parameter to assign generated keys to. "
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
+ paramMap.keySet());
}
// 获得真正的参数值
String paramName = keyProperties[0].substring(0, firstDot);
Object param;
if (paramMap.containsKey(paramName)) {
param = paramMap.get(paramName);
} else {
throw new ExecutorException("Could not find parameter '" + paramName + "'. "
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
+ paramMap.keySet());
}
// Remove param name from 'keyProperty' string. e.g. 'param.id' -> 'id'
// 获得主键的属性的配置
String[] modifiedKeyProperties = new String[keyProperties.length];
for (int i = 0; i < keyProperties.length; i++) {
if (keyProperties[i].charAt(firstDot) == '.' && keyProperties[i].startsWith(paramName)) {
modifiedKeyProperties[i] = keyProperties[i].substring(firstDot + 1);
} else {
throw new ExecutorException("Assigning generated keys to multiple parameters is not supported. "
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
+ paramMap.keySet());
}
}
// 设置主键们,到参数 param 中
assignKeysToParam(configuration, rs, modifiedKeyProperties, param);
}
```
- `<1>` 处,需要有 `.` 。例如:`@Options(useGeneratedKeys = true, keyProperty = "country.id")` 。
- `<2>` 处,获得真正的参数值。
- `<3>` 处,获得主键的属性的配置。
- `<4>` 处,调用 `#assignKeysToParam(...)` 方法,设置主键们,到参数 `param` 中。所以,后续流程,又回到了 [「3.4.2」](https://svip.iocoder.cn/MyBatis/executor-3/#) 咧。
- 关于这个方法,胖友自己模拟下这个情况,调试下会比较好理解。😈 当然,也可以不理解,嘿嘿。
## 3.5 populateKeys
```
// Jdbc3KeyGenerator.java
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
// 遍历 keyProperties
for (int i = 0; i < keyProperties.length; i++) {
// 获得属性名
String property = keyProperties[i];
// 获得 TypeHandler 对象
TypeHandler<?> th = typeHandlers[i];
if (th != null) {
// 从 rs 中,获得对应的 值
Object value = th.getResult(rs, i + 1);
// 设置到 metaParam 的对应 property 属性种
metaParam.setValue(property, value);
}
}
}
```
- 代码比较简单,胖友看下注释即可。
# 4. SelectKeyGenerator
`org.apache.ibatis.executor.keygen.SelectKeyGenerator` ,实现 KeyGenerator 接口,基于从数据库查询主键的 KeyGenerator 实现类,适用于 Oracle、PostgreSQL 。
## 4.1 构造方法
```
// SelectKeyGenerator.java
public static final String SELECT_KEY_SUFFIX = "!selectKey";
/**
* 是否在 before 阶段执行
*
* true before
* after after
*/
private final boolean executeBefore;
/**
* MappedStatement 对象
*/
private final MappedStatement keyStatement;
public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
this.executeBefore = executeBefore;
this.keyStatement = keyStatement;
}
```
## 4.2 processBefore
```
// SelectKeyGenerator.java
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
```
- 调用 `#processGeneratedKeys(...)` 方法。
## 4.3 processAfter
```
// SelectKeyGenerator.java
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (!executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
```
- 也是调用 `#processGeneratedKeys(...)` 方法。
## 4.4 processGeneratedKeys
```
// SelectKeyGenerator.java
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
// <1> 有查询主键的 SQL 语句,即 keyStatement 对象非空
if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
String[] keyProperties = keyStatement.getKeyProperties();
final Configuration configuration = ms.getConfiguration();
final MetaObject metaParam = configuration.newMetaObject(parameter);
// Do not close keyExecutor.
// The transaction will be closed by parent executor.
// <2> 创建执行器,类型为 SimpleExecutor
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
// <3> 执行查询主键的操作
List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
// <4.1> 查不到结果,抛出 ExecutorException 异常
if (values.size() == 0) {
throw new ExecutorException("SelectKey returned no data.");
// <4.2> 查询的结果过多,抛出 ExecutorException 异常
} else if (values.size() > 1) {
throw new ExecutorException("SelectKey returned more than one value.");
} else {
// <4.3> 创建 MetaObject 对象,访问查询主键的结果
MetaObject metaResult = configuration.newMetaObject(values.get(0));
// <4.3.1> 单个主键
if (keyProperties.length == 1) {
// 设置属性到 metaParam 中,相当于设置到 parameter 中
if (metaResult.hasGetter(keyProperties[0])) {
setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
} else {
// no getter for the property - maybe just a single value object
// so try that
setValue(metaParam, keyProperties[0], values.get(0));
}
// <4.3.2> 多个主键
} else {
// 遍历,进行赋值
handleMultipleProperties(keyProperties, metaParam, metaResult);
}
}
}
} catch (ExecutorException e) {
throw e;
} catch (Exception e) {
throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
}
}
```
- `<1>` 处,有查询主键的 SQL 语句,即 `keyStatement` 对象非空。
- `<2>` 处,创建执行器,类型为 SimpleExecutor 。
- 【重要】 `<3>` 处,调用 `Executor#query(...)` 方法,执行查询主键的操作。😈 简单脑暴下,按照 SelectKeyGenerator 的思路,岂不是可以可以接入 SnowFlake 算法,从而实现分布式主键。
- `<4.1>` 处,查不到结果,抛出 ExecutorException 异常。
- `<4.2>` 处,查询的结果过多,抛出 ExecutorException 异常。
- `<4.3>` 处,创建 MetaObject 对象,访问查询主键的结果。
- `<4.3.1>` 处,**单个主键**,调用 `#setValue(MetaObject metaParam, String property, Object value)` 方法,设置属性到 `metaParam` 中,相当于设置到 `parameter` 中。代码如下:
```
// SelectKeyGenerator.java
private void setValue(MetaObject metaParam, String property, Object value) {
if (metaParam.hasSetter(property)) {
metaParam.setValue(property, value);
} else {
throw new ExecutorException("No setter found for the keyProperty '" + property + "' in " + metaParam.getOriginalObject().getClass().getName() + ".");
}
}
```
- 简单,胖友自己瞅瞅。
- `<4.3.2>` 处,**多个主键**,调用 `#handleMultipleProperties(String[] keyProperties, MetaObject metaParam, MetaObject metaResult)` 方法,遍历,进行赋值。代码如下:
```
// SelectKeyGenerator.java
private void handleMultipleProperties(String[] keyProperties,
MetaObject metaParam, MetaObject metaResult) {
String[] keyColumns = keyStatement.getKeyColumns();
// 遍历,进行赋值
if (keyColumns == null || keyColumns.length == 0) {
// no key columns specified, just use the property names
for (String keyProperty : keyProperties) {
setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));
}
} else {
if (keyColumns.length != keyProperties.length) {
throw new ExecutorException("If SelectKey has key columns, the number must match the number of key properties.");
}
for (int i = 0; i < keyProperties.length; i++) {
setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));
}
}
}
```
- 最终,还是会调用 `#setValue(...)` 方法,进行赋值。
## 4.5 示例
- [《MyBatis + Oracle 实现主键自增长的几种常用方式》](https://blog.csdn.net/wal1314520/article/details/77132305)
- [《mybatis + postgresql 返回递增主键的正确姿势及勘误》](https://blog.csdn.net/cdnight/article/details/72735108)
# 5. NoKeyGenerator
`org.apache.ibatis.executor.keygen.NoKeyGenerator` ,实现 KeyGenerator 接口,空的 KeyGenerator 实现类,即无需主键生成。代码如下:
```
// NoKeyGenerator.java
public class NoKeyGenerator implements KeyGenerator {
/**
* A shared instance.
* @since 3.4.3
*/
public static final NoKeyGenerator INSTANCE = new NoKeyGenerator();
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// Do Nothing
}
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// Do Nothing
}
}
```
# 666. 彩蛋
简单小文,嘿嘿。
参考和推荐如下文章:
- 祖大俊 [《Mybatis3.3.x技术内幕十四Mybatis之KeyGenerator》](https://my.oschina.net/zudajun/blog/673612)
- 祖大俊
《Mybatis3.3.x技术内幕十五Mybatis之foreach批量insert返回主键id列表修复Mybatis返回null的bug
强烈推荐
- 目前已经修复,参见 https://github.com/mybatis/mybatis-3/pull/324 。
- 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「3.4 KeyGenerator」](https://svip.iocoder.cn/MyBatis/executr-3/#) 小节