一、简介
在Mybatis中,insert语句执行时,可以返回自动产生的主键,这里便是使用KeyGenerator来完成的。本篇内容就来分析Mybatis中的主键生成策略是怎么样起作用的。
首先,不同的数据库产品对应的主键生成策略不一样,主要分为两类:一类是在执行insert 语句之前必须明确指定主键的,比如: Oracle 、DB2 等数据库;一类是可以不指定主键,而在插入过程中由数据库自动生成自增主键,比如:MYSQL, Postgresql等数据库。
在KeyGenerator中,针对不同的数据库产品,提供了不同的方法进行处理。其中,processBefore()方法,在执行insert之前执行,一般用于Oracle 、DB2 等数据库;processAfter()方法,在执行insert之后执行,一般用于MYSQL, Postgresql等数据库。(注:后续 分析源码发现,其实两个方法只是明确是在insert执行前或执行后执行,和数据库没有关系)
//KeyGenerator.java
/**
* 主键生成器接口,有三个实现类:
* 1、 {@link Jdbc3KeyGenerator}<br>
* 2、{@link NoKeyGenerator}<br>
* 3、{@link SelectKeyGenerator}<br>
* @author Clinton Begin
*/
public interface KeyGenerator {
/**
* 针对Sequence主键而言,在执行insert sql前必须指定一个主键值给要插入的记录,
* 如Oracle、DB2,KeyGenerator提供了processBefore()方法。
* @param executor
* @param ms
* @param stmt
* @param parameter
*/
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
/**
* 针对自增主键的表,在插入时不需要主键,而是在插入过程自动获取一个自增的主键,
* 比如MySQL,Postgresql,KeyGenerator提供了processAfter()方法
* @param executor
* @param ms
* @param stmt
* @param parameter
*/
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
二、KeyGenerator实现类
Mybatis提供了三个KeyGenerator接口的实现类,分别是NoKeyGenerator、Jdbc3KeyGenerator、SelectKeyGenerator。其中,
- NoKeyGenerator:默认空实现,不对主键单独处理;
- Jdbc3KeyGenerator:主要用于数据库的自增主键,比如 MySQL、PostgreSQL;
- SelectKeyGenerator:主要用于数据库不支持自增主键的情况,比如 Oracle、DB2;
类图:
三、实现类Jdbc3KeyGenerator
Jdbc3KeyGenerator主要用于支持主键自增的数据库,比如MySQL、PostgreSQL、SQL Server等。
1、useGeneratedKeys属性的用法
首先,如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置到目标属性上就 OK 了。例如:
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
注:需要把Author表中主键对应的字段设置成自动生成的列类型
2、Jdbc3KeyGenerator类详解
Jdbc3KeyGenerator实现类就是处理那些支持主键自增的数据库的,主要用来支持上述提到的通过useGeneratedKeys属性配置主键生成策略的实现。
- 变量
Jdbc3KeyGenerator实现类中定义了一个静态的Jdbc3KeyGenerator变量INSTANCE,供全局使用,即全局共享这一个静态变量。
//Jdbc3KeyGenerator.java
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
- processBefore()方法
因为Jdbc3KeyGenerator实现类是处理那些支持主键自增的数据库的,所以在insert语句执行前,不做任何处理,即该方法为空实现。 - processAfter()方法
processAfter()方法是通过调用processBatch()方法实现想要的业务逻辑。调用processBatch()方法前,首先通过getParameters()方法处理参数,然后再调用processBatch()方法。
//Jdbc3KeyGenerator.java
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, getParameters(parameter));
}
- getParameters()方法
将传入的parameter实参转换成Collection类型对象。
//Jdbc3KeyGenerator.java
private Collection<Object> getParameters(Object parameter) {
Collection<Object> parameters = null;
if (parameter instanceof Collection) {
parameters = (Collection) parameter;
} else if (parameter instanceof Map) {
Map parameterMap = (Map) parameter;
if (parameterMap.containsKey("collection")) {
parameters = (Collection) parameterMap.get("collection");
} else if (parameterMap.containsKey("list")) {
parameters = (List) parameterMap.get("list");
} else if (parameterMap.containsKey("array")) {
parameters = Arrays.asList((Object[]) parameterMap.get("array"));
}
}
if (parameters == null) {
parameters = new ArrayList<Object>();
parameters.add(parameter);
}
return parameters;
}
- processBatch()方法
processBatch()方法主要实现了一下逻辑:
1、获取数据库自动生成的主键
2、获取主键对应的属性名称
3、检测数据库生成的主键的列数与keyProperties属性指定的列数是否匹配
4、for循环,处理参数。因为在insert语句中,可能存在同时添加多条数据的情况,每次循环即处理一条添加的记录。
5、在for循环过程中,首先根据参数,获取对应的typeHandler,然后根据typeHandler获取主键字段对应的值,最后通过MetaObject的setValue()方法,把参数及其参数值写入到对应对象中。
//Jdbc3KeyGenerator.java
public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
ResultSet rs = null;
try {
//获取数据库自动生成的主键,如果没有生成主键,则返回结采集为空
rs = stmt.getGeneratedKeys();
final Configuration configuration = ms.getConfiguration();
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
//获得keyProperties属性指定的属性名称,它表示主键对应的属性名称
final String[] keyProperties = ms.getKeyProperties();
//获取ResultSet的元数据信息
final ResultSetMetaData rsmd = rs.getMetaData();
TypeHandler<?>[] typeHandlers = null;
//检测数据库生成的主键的列数与keyProperties属性指定的列数是否匹配
if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
for (Object parameter : parameters) {
// there should be one row for each statement (also one for each parameter)
if (!rs.next()) {
break;
}
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (typeHandlers == null) {//获取主键字段分别对应的TypeHandler
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
}
//将生成的主键设直到用户传入的参数的对应位置
populateKeys(rs, metaParam, keyProperties, typeHandlers);
}
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
} finally {
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
// ignore
}
}
}
}
- getTypeHandlers()方法
获取对应的TypeHandler。
//Jdbc3KeyGenerator.java
private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
for (int i = 0; i < keyProperties.length; i++) {
if (metaParam.hasSetter(keyProperties[i])) {
TypeHandler<?> th;
try {
Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
th = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
} catch (BindingException e) {
th = null;
}
typeHandlers[i] = th;
}
}
return typeHandlers;
}
- populateKeys()方法
将生成的主键设置到用户传入的参数的对应位置。
//Jdbc3KeyGenerator.java
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
for (int i = 0; i < keyProperties.length; i++) {
String property = keyProperties[i];
TypeHandler<?> th = typeHandlers[i];
if (th != null) {
Object value = th.getResult(rs, i + 1);
metaParam.setValue(property, value);
}
}
}
四、实现类SelectKeyGenerator
SelectKeyGenerator实现类主要用于数据库不支持自增主键的情况,比如 Oracle、DB2等。
1、 <selectKey>节点用法
对于不支持自动生成类型的数据库或可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键。这里有一个简单(甚至很傻)的示例,它可以生成一个随机 ID(你最好不要这么做,但这里展示了 MyBatis 处理问题的灵活性及其所关心的广度)。在下面的示例中,selectKey 元素中的语句将会首先运行,Author 的 id 会被设置,然后插入语句会被调用。这可以提供给你一个与数据库中自动生成主键类似的行为,同时保持了 Java 代码的简洁。
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
2、 SelectKeyGenerator类详解
对于不支持自动生成自增主键的数据库,例如Oracle 数据库,用户可以利用Mybatis提供的SelectkeyGenerator 来生成主键, SelectkeyGenerator 也可以实现类似于Jdbc3KeyGenerator 提供的、获取数据库自动生成的主键的功能。
- 变量
//SelectKeyGenerator.java
/**
* <selectKey>节点,解析过程中,生成id时,使用的默认后缀
*/
public static final String SELECT_KEY_SUFFIX = "!selectKey";
/**
* 标识<selectKey>节点中定义的SQL语句是在insert语句之前执行还是之后执行
*/
private final boolean executeBefore;
/**
* <selectKey>节点中定义的SQL语句所对应的MappedStatement对象。
* 该MappedStatement对象是在解析<selectKey>节点时创建的。
* 该SQL语句用于获取insert语句中使用的主键。
*/
private final MappedStatement keyStatement;
- 构造函数
//SelectKeyGenerator.java
public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
this.executeBefore = executeBefore;
this.keyStatement = keyStatement;
}
- processBefore()方法、processAfter()方法
在processBefore()方法和processAfter()方法的实现都是调用processGeneratedKeys()方法。通过标识<selectKey>节点中定义的SQL语句是在insert语句之前执行还是之后执行的变量executeBefore来确定,执行processBefore()或processAfter()方法。(根据方法的逻辑,支持自增主键的数据库,理论上应该也可以使用该方法,进行主键生成,未验证)
//SelectKeyGenerator.java
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (!executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
- processGeneratedKeys()方法
执行<selectKey>节点中配置的SQL语句,获取insert语句中用到的主键并映射成对象,然后按照配置,将主键对象中对应的属性设置到用户参数中。
//SelectKeyGenerator.java
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
//获取<selectKey>节点的keyProperties配置的属性名称,它表示主键对应的属性
String[] keyProperties = keyStatement.getKeyProperties();
final Configuration configuration = ms.getConfiguration();
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (keyProperties != null) {
// Do not close keyExecutor.
// The transaction will be closed by parent executor.
//创建Executor对象,并执行keyStatement字段中记录的SQL语句,并得到主键对象
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 metaResult = configuration.newMetaObject(values.get(0));
if (keyProperties.length == 1) {
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));
}
} 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);
}
}
- setValue()方法
通过MetaObject实例对象,实现为指定对象的字段赋值。
//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() + ".");
}
}
- handleMultipleProperties()方法
处理主键有多列的情况,其实现是从主键对象中取出指定属性,并设直到用户参数的对应属性中。
//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]));
}
}
}