MyBatis中灵活的主键自增功能使用及其原理详解
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/79/ )。
在进行数据插入操作时,经常需要一个自增生成的主键编号,这既能保证主键的唯一性,又能保证主键的连续性。
许多数据库都支持主键自增功能,如MySQL数据库、SQL Server数据库等。当然也有一些数据库不支持主键自增功能,如Oracle数据库。MyBatis的executor包中的keygen子包兼容以上这两种情况。
接下来我们通过MyBatis源码详细了解MyBatis的主键自增功能的使用方法和实现原理。相关内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。
主键自增功能在MyBatis的executor包的keygen子包中,一共包含下图所示的四个类或接口。
KeyGenerator作为接口提供了两个方法,即processBefore方法和processAfter方法。关于这两个方法的实现细节我们会在接下来的小节中分别介绍。
NoKeyGenerator不提供任何主键自增功能,其processBefore方法和processAfter方法均为空方法。因此,我们不再介绍该类。
1 主键自增的配置与生效
在阅读MyBatis的主键自增相关代码之前,我们先了解在怎么在MyBatis中启用主键自增功能。
通过KeyGenerator的类图我们知道,MyBatis中的KeyGenerator实现类一共有三种:Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator。在实际使用时,这三种实现中只能有一种实现生效。而如果生效的是NoKeyGenerator,则代表不具有任何的主键自增功能。
要启用Jdbc3KeyGenerator可以在配置文件中增加如下所示的配置:
<setting name="useGeneratedKeys" value="true"/>
或者直接在相关语句上启用useGeneratedKeys,如下所示。
<insert id="addUser_A" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `user`
(`name`,`email`,`age`,`sex`,`schoolName`)
VALUES
(#{name},#{email},#{age},#{sex},#{schoolName})
</insert>
如果要启用SelectKeyGenerator则需要在SQL语句前加一段selectKey标签,如下所示。
<insert id="addUser_B" parameterType="User">
<selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO `user`
(`name`,`email`,`age`,`sex`,`schoolName`)
VALUES
(#{name},#{email},#{age},#{sex},#{schoolName})
</insert>
如果某一条语句同时设置了useGeneratedKeys和selectKey的话,则是后者生效。
以上各个配置项的作用范围、优先级等结论,均可以通过阅读下面的源码得出。下面的源码在XMLStatementBuilder类中,在集成开发软件的帮助下,我们可以通过查找KeyGenerator的引用找到。这段代码就是主键自增功能被解析的地方。
// 处理SelectKey节点,在这里会将KeyGenerator加入到Configuration.keyGenerators中
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 原注释:此时,<selectKey> 和 <include> 节点均已被解析完毕并被删除
// 进行SQL解析
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 判断是否已经有解析好的KeyGenerator
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 全局或者本语句只要启用自动key生成,则使用key生成
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
processSelectKeyNodes方法经过最终解析了selectKey节点的信息并在解析完成后将selectKey节点从XML中删除了。而解析出来的信息而放入了configuration的keyGenerators中。之后,如果没有解析好的KeyGenerator,则会根据useGeneratedKeys判断是否使用Jdbc3KeyGenerator。
最终,KeyGenerator信息会被保存在整个Statement中。在Statement的执行时,直接调用KeyGenerator中的processBefore方法和processAfter方法即可,必然会有Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator三者中的一个来实际执行这两个方法。
接下来我们介绍Jdbc3KeyGenerator和SelectKeyGenerator的源码。
2 Jdbc3KeyGenerator
2.1 Jdbc3KeyGenerator的功能
Jdbc3KeyGenerator是为具有主键自增功能的数据库准备的。说到这里大家可能会疑惑,既然数据库已经支持主键自增了,那Jdbc3KeyGenerator存在的意义是什么?
它存在的意义是提供自增主键的回写功能。
接下来我们通过示例来说明此功能。首先我们对user表中的id字段启用主键自增功能,然后下所示配置XML映射文件。这里并没有启用主键自增功能。
<insert id="addUser_D" parameterType="User">
INSERT INTO `user`
(`name`,`email`,`age`,`sex`,`schoolName`)
VALUES
(#{name},#{email},#{age},#{sex},#{schoolName})
</insert>
然后运行下面的Java语句。
User user04 = new User("李二壮", 0, "KEYUAN SCHOOL");
// 使用NoKeyGenerator,即不使用主键自增功能
System.out.println("before insert :" + user04.toString());
result = session.insert("com.github.yeecode.mybatisdemo.dao.UserDao.addUser_D", user04);
System.out.println("insert result : " + result);
System.out.println("after insert :" + user04.toString());
因为在数据库中对id字段启用了自增功能,所以在数据插入操作结束后,数据库中的user04的id字段会被设置为一个数值。然而,我们Java程序中的user04对象却不会被更新,因此输出的user04的id值仍然为null。结果如下。
Jdbc3KeyGenerator提供的回写功能,能够将数据库中产生的id值回写给Java对象本身。我们可以通过下面的设置启用Jdbc3KeyGenerator。
<insert id="addUser_A" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `user`
(`name`,`email`,`age`,`sex`,`schoolName`)
VALUES
(#{name},#{email},#{age},#{sex},#{schoolName})
</insert>
然后同样执行代码22-18所示的操作便可以得到下图所示的结果:
因此Jdbc3KeyGenerator所做的工作就是在Java对象插入完成后,将数据库自增产生的id读取出来,然后回写给Java对象本身。其功能示意图如下所示,其中粗实线表示的工作就是Jdbc3KeyGenerator完成的。
2.2 Jdbc3KeyGenerator的原理
Jdbc3KeyGenerator的工作是在数据库主键自增结束后,将自增出来的主键读取出来并赋给Java对象。这些工作都是在数据插入完成后进行的,即在processAfter方法中进行。而processBefore方法中不需要进行任何操作。
processAfter方法直接调用了processBatch方法。在阅读processBatch方法前我们先复习一个小的知识:Statement对象的getGeneratedKeys方法能返回此语句操作自增生成的主键,如果此语句没有产生自增主键则结果为空ResultSet对象。该内容在《通用源码阅读指导书——MyBatis源码详解》的17.1.5 Statement章节做过介绍。
接下来我们直接阅读下面代码给出的带注释的processBatch方法源码。该方法的主要工作就是调用Statement对象的getGeneratedKeys方法获取数据库自增生成的主键,然后将主键赋给实参以达到回写的目的。
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
// 拿到主键的属性名
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
// 没有主键则无需操作
return;
}
// 调用Statement对象的getGeneratedKeys方法获取自动生成的主键值
try (ResultSet rs = stmt.getGeneratedKeys()) {
// 获取输出结果的描述信息
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// 主键数目比结果的总字段数目还多,则发生了错误。
// 但因为此处是获取主键这样的附属操作,因此忽略错误,不影响主要工作
} else {
// 调用子方法,将主键值赋给实参
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}
2.3 SelectKeyGenerator
Jdbc3KeyGenerator其实并没有真正的生成自增主键,而只是将数据库自增出的主键值回写到了Java对象中。因此,面对不支持主键自增功能的数据库时,Jdbc3KeyGenerator将无能为力。这时就需要SelectKeyGenerator类,因为它可以真正地生成自增的主键。
SelectKeyGenerator实现了processBefore和processAfter这两个方法。然而,如下面代码所示,这两个方法均直接调用了子方法processGeneratedKeys,这可能会让看到源码的我们感到疑惑。
/**
* 数据插入前进行的操作
* @param executor 执行器
* @param ms 映射语句对象
* @param stmt Statement对象
* @param parameter SQL语句实参对象
*/
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
/**
* 数据插入后进行的操作
* @param executor 执行器
* @param ms 映射语句对象
* @param stmt Statement对象
* @param parameter SQL语句实参对象
*/
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (!executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
为了解答这个疑惑,我们先介绍SelectKeyGenerator的功能。
SelectKeyGenerator的功能描述起来很简单:先执行一段特定的SQL语句获取一个值,然后将该值赋给Java对象的自增属性。
然而,SelectKeyGenerator这一功能的执行时机分为两种,这两种执行时机通过配置二选一:
- 在数据插入之前执行。执行完特定的SQL语句并将值赋给对象的自增属性后,再将这个完整的对象插入到数据库中。而这种操作又分为两种情况:
- 如果数据库没有设置或者不支持主键自增,则完整的对象会被完整地插入到数据库中。这是SelectKeyGenerator最常用的使用场景。
- 如果数据库设置了主键自增,则刚才特定SQL语句生成的自增属性值会被数据库自身的自增值覆盖掉。这种情况下,Java对象的自增属性值可能会和数据库中的自增属性值不一致,因此是错误的。这种情况下,建议使用Jdbc3KeyGenerator的回写功能。
- 在数据插入之后执行。对象插入数据库结束后,Java对象的自增属性被设置成特定SQL语句的执行结果。这种操作也分为两种情况:
- 如果数据库不支持主键自增,则之前被插入到数据库中的对象的自增属性是没有被赋值的,而Java对象的自增属性却被赋值了,这会导致不一致。这种操作是错误的。
- 果数据库设置了主键自增,则数据库自增生成的值和SQL语句执行产生的值可能不一样。不过我们一般通过设置特定SQL语句来保证两者一致,这其实和Jdbc3KeyGenerator的回写功能类似。
可见SelectKeyGenerator的功能描述起来简单又灵活,但是因为执行时机、数据库状况等不同可能产生多种情况,需要在使用者自己把握。SelectKeyGenerator的功能示意图如下所示。
processBefore和processAfter这两个方法都直接调用了processGeneratedKeys方法,所以processGeneratedKeys方法的功能就是执行一段SQL语句后获取一个值,然后将该值赋给Java对象的自增属性。
在阅读processGeneratedKeys方法前,我们先对SelectKeyGenerator中的属性进行介绍,如下所示。
// 用户生成主键的SQL语句的特有标志,该标志会追加在用于生成主键的SQL语句的id的后方
public static final String SELECT_KEY_SUFFIX = "!selectKey";
// 插入前执行还是插入后执行
private final boolean executeBefore;
// 用户生成主键的SQL语句
private final MappedStatement keyStatement;
下面是processGeneratedKeys方法的带注释的源码。
/**
+ 执行一段SQL语句后获取一个值,然后将该值赋给Java对象的自增属性
*
+ @param executor 执行器
+ @param ms 插入操作的SQL语句(不是生成主键的SQL语句)
+ @param parameter 插入操作的对象
*/
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
try {
// keyStatement为生成主键的SQL语句;keyStatement.getKeyProperties()拿到的是要自增的属性
if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
// 要自增的属性
String[] keyProperties = keyStatement.getKeyProperties();
final Configuration configuration = ms.getConfiguration();
final MetaObject metaParam = configuration.newMetaObject(parameter);
if (keyProperties != null) {
// 为生成主键的SQL语句创建执行器keyExecutor。
// 原注释:不要关闭keyExecutor,因为它会被父级的执行器关闭
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
// 执行SQL语句,得到主键值
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])) {
// 从metaResult中用getter得到主键值
setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
} else {
// 可能返回的直接就是主键值本身
setValue(metaParam, keyProperties[0], values.get(0));
}
} else {
// 要把执行SQL得到的值赋给多个属性
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);
}
}
这样,我们对SelectKeyGenerator类的功能以及如何实现这些功能够进行了详细的介绍。因此,我们可以将SelectKeyGenerator作为Jdbc3KeyGenerator的升级版或者自由定制版:
- SelectKeyGenerator可以设置为插入前执行并实现主键的主动生成,并且可以通过SQL语句设置主键生成方式。这是Jdbc3KeyGenerator没有的功能。
- SelectKeyGenerator可以设置为插入后执行。通过将主键生成SQL设置为类似“
SELECT LAST_INSERT_ID()”的语句便可以实现主键回写功能。
以上内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。
这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注释。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。推荐给大家。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。
