22 KiB
精尽 MyBatis 源码分析 —— SQL 执行(三)之 KeyGenerator
1. 概述
本文,我们来分享 SQL 执行的第三部分,keygen
包。整体类图如下:之 KeyGenerator.assets/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 有三个子类,如下图所示:之 KeyGenerator.assets/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」 。<3.2>
处,调用#assignKeysToOneOfParams(...)
方法,设置主键们,到参数parameter
中。详细解析,见 「3.4.3 assignKeysToOneOfParams」 。
-
<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」 。
-
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」 咧。- 关于这个方法,胖友自己模拟下这个情况,调试下会比较好理解。😈 当然,也可以不理解,嘿嘿。
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 示例
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之foreach批量insert,返回主键id列表(修复Mybatis返回null的bug)》
强烈推荐
- 目前已经修复,参见 https://github.com/mybatis/mybatis-3/pull/324 。
-
徐郡明 《MyBatis 技术内幕》 的 「3.4 KeyGenerator」 小节