文档章节

Mybatis3.3.x技术内幕(十四):Mybatis之KeyGenerator

祖大俊
 祖大俊
发布于 2016/05/11 19:26
字数 1671
阅读 1259
收藏 10

在Mybatis中,执行insert操作时,如果我们希望返回数据库生成的自增主键值,那么就需要使用到KeyGenerator对象。

需要注意的是,KeyGenerator的作用,是返回数据库生成的自增主键值,而不是生成数据库的自增主键值。返回的主键值放到哪儿呢?放到parameter object的主键属性上。

下面看看其接口定义。

public interface KeyGenerator {
  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}

接口定义还是比较简单的,就是在insert前、insert后,策略处理主键值。

 

 

 

(Made In IntelliJ IDEA IDE)

Jdbc3KeyGenerator:用于处理数据库支持自增主键的情况,如MySQL的auto_increment。

NoKeyGenerator:空实现,不需要处理主键。

SelectKeyGenerator:用于处理数据库不支持自增主键的情况,比如Oracle的sequence序列。

上面都比较泛泛而谈,我们来点实际的,看看它们都是如何工作的。

1. JDBC实现insert后,返回自增主键值的原理

Class.forName("com.mysql.jdbc.Driver");

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "123");
conn.setAutoCommit(false);
PreparedStatement pstm = conn.prepareStatement("insert into students(name, email) values(?, ?)",
Statement.RETURN_GENERATED_KEYS);

pstm.setString(1, "name1");
pstm.setString(2, "email1");
pstm.addBatch();
pstm.setString(1, "name2");
pstm.setString(2, "email2");
pstm.addBatch();
pstm.executeBatch();
// 返回自增主键值
ResultSet rs = pstm.getGeneratedKeys();
while (rs.next()) {
		Object value = rs.getObject(1);
		System.out.println(value);
	}
conn.commit();
rs.close();
pstm.close();
conn.close();

output:
246
247

以上代码,仅作为演示使用。Mybatis是对JDBC的封装,其Jdbc3KeyGenerator类,就是使用上面的原理,来返回数据库生成的主键值的。

2. Jdbc3KeyGenerator源码解读

public class Jdbc3KeyGenerator implements KeyGenerator {

  @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) {
    processBatch(ms, stmt, getParameters(parameter));
  }

  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();
      final String[] keyProperties = ms.getKeyProperties();
      final ResultSetMetaData rsmd = rs.getMetaData();
      TypeHandler<?>[] typeHandlers = null;
      if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
        // 给参数object对象的属性赋主键值(批量插入,可能是多个)
        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) {
            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
        }
      }
    }
  }
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
    // 主键字段,可能是多个(一般情况下,是一个)
    for (int i = 0; i < keyProperties.length; i++) {
      TypeHandler<?> th = typeHandlers[i];
      if (th != null) {
        Object value = th.getResult(rs, i + 1);
       // 反射赋值
        metaParam.setValue(keyProperties[i], value);
      }
    }
  }
//...

Mapper.Xml配置方式。

<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="Student">

3. NoKeyGenerator源码解读

完全是空实现,没啥可说的。

4. SelectKeyGenerator的原理

	<insert id="insertStudent" parameterType="Student" >
		<selectKey keyProperty="studId" resultType="int" order="BEFORE"> 
			SELECT ELEARNING.STUD_ID_SEQ.NEXTVAL FROM DUAL 
		</selectKey>
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES(#{studId}, #{name},
		#{email}, #{dob}, #{phone})
	</insert>

在执行insert之前,先发起一个sql查询,将返回的序列值赋值给Student的stuId属性,然后再执行insert操作,这样表中的stud_id字段就有值了。order="BEFORE"表示insert前执行,比如取sequence序列值;order="AFTER"表示insert之后执行,比如使用触发器给主键stud_id赋值。比较简单,我就不再贴源码了。

注意:由于selectKey本身返回单个序列主键值,也就无法支持批量insert操作并返回主键id列表了。如果要执行批量insert,请选择使用for循环执行多次插入操作。

5. KeyGenerator的创建过程

每一个MappedStatement,都有一个非空的KeyGenerator引用。

org.apache.ibatis.mapping.MappedStatement.Builder.Builder()构造方法赋初始值源码。

mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()覆盖KeyGenerator初始值的源码。

String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
if (configuration.hasKeyGenerator(keyStatementId)) {
      // 表示存在selectKey获取主键值方式
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseSelectKeyNode()解析<selectKey>元素,构建SelectKeyGenerator的源码。

MappedStatement keyStatement = configuration.getMappedStatement(id, false);
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));

因此,只有SelectKeyGenerator会保存至Configuration对象的Map<String, KeyGenerator> keyGenerators属性当中。<selectKey>元素,会被Mybatis解析为一个MappedStatement对象,并作为构造参数传递至SelectKeyGenerator内保存起来。

public class SelectKeyGenerator implements KeyGenerator {
  
  public static final String SELECT_KEY_SUFFIX = "!selectKey";
  private boolean executeBefore;
  private MappedStatement keyStatement;
//...

Map<String, KeyGenerator> keyGenerators的存储结构如下。

{insertStudent!selectKey=org.apache.ibatis.executor.keygen.SelectKeyGenerator@59d016c9, 
com.mybatis3.mappers.StudentMapper.insertStudent!selectKey=org.apache.ibatis.executor.keygen.SelectKeyGenerator@59d016c9}

至此,每一个MappedStatement对象,都恰当的绑定了一个KeyGenerator对象,就可以开始工作了。

6. KeyGenerator的使用过程

keyGenerator.processBefore()方法调用时机。

org.apache.ibatis.executor.statement.BaseStatementHandler.BaseStatementHandler()构造方法源码。

if (boundSql == null) {
      // 调用keyGenerator.processBefore()方法
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }
// ...

 protected void generateKeys(Object parameter) {
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processBefore(executor, mappedStatement, null, parameter);
  }

即,创建StatementHandler对象时,就会执行keyGenerator.processBefore()方法。keyGenerator.processAfter()方法,自然就是Statement执行后执行了。

org.apache.ibatis.executor.statement.SimpleStatementHandler.update(Statement)方法源码。其他的StatementHandler都是类似的。

 @Override
  public int update(Statement statement) throws SQLException {
    String sql = boundSql.getSql();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    int rows;
    if (keyGenerator instanceof Jdbc3KeyGenerator) {
      statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
      rows = statement.getUpdateCount();
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else if (keyGenerator instanceof SelectKeyGenerator) {
      statement.execute(sql);
      rows = statement.getUpdateCount();
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else {
      statement.execute(sql);
      rows = statement.getUpdateCount();
    }
    return rows;
  }

7. 批量插入,返回主键id列表

for (Student student : students) {
	studentMapper.insertStudent(student);
}

对的,你没看错,就是像上面这样for循环逐一insert操作的,此时,如果你考虑性能的话,可以使用BatchExecutor来完成,当然了,其他的Executor也是可以的。

如果文章就像上面这样写,那么就完全失去了写文章的价值,上面的for循环,谁都懂这么操作可以实现,但是,很多人想要的并不是这个例子,而是另外一种批量插入操作,返回主键id列表。那么,看第8条。

8. Mybatis批量插入,返回主键id列表为null

<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList">
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES
	<foreach collection="list" item="item" index="index" separator=","> 
        	(#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
    	</foreach> 
</insert>

很多同学,包括开源中国社区,都遇到使用上面的批量insert操作,返回的主键id列表是null的问题,很多人得出结论:Mybatis不支持这种形式的批量插入并返回主键id列表。真是这样吗?

我必须明确的跟大家说,Mybatis是支持上述形式的批量插入,且可以正确返回主键id列表的。之所以返回null值,是Mybatis框架的一个bug,下一篇将具体讲述产生这个bug的原因,以及如何修复它。

版权提示:文章出自开源中国社区,若对文章感兴趣,可关注我的开源中国社区博客(http://my.oschina.net/zudajun)。(经过网络爬虫或转载的文章,经常丢失流程图、时序图,格式错乱等,还是看原版的比较好)

© 著作权归作者所有

共有 人打赏支持
祖大俊
粉丝 640
博文 32
码字总数 52477
作品 0
昌平
加载中

评论(3)

祖大俊
祖大俊

引用来自“祖大俊”的评论

新的博客编辑器,丑的要命,还强制我使用,真有点受不了。

引用来自“hwa01”的评论

// 返回自增主键值
ResultSet rs = pstm.getGeneratedKeys();
Object value = rs.getObject(1);
返回的value即此次插入的主键。执行一条insert语句,这里面有执行select操作吗,它是如何知道此次插入记录的自增主键的?
这里没有执行select操作,pstm.getGeneratedKeys()是JDBC的驱动接口,由数据库厂商去实现,它如何知道此次插入记录的自增主键?作为数据库实现厂商来说,它肯定是知道的。
h
hwa01

引用来自“祖大俊”的评论

新的博客编辑器,丑的要命,还强制我使用,真有点受不了。
// 返回自增主键值
ResultSet rs = pstm.getGeneratedKeys();
Object value = rs.getObject(1);
返回的value即此次插入的主键。执行一条insert语句,这里面有执行select操作吗,它是如何知道此次插入记录的自增主键的?
祖大俊
祖大俊
新的博客编辑器,丑的要命,还强制我使用,真有点受不了。
Mybatis3.3.x技术内幕(十):Mybatis初始化流程(下)

Mybatis初始化过程中,解析parameterMap、resultMap、"select|insert|update|delete"元素,无疑是重头戏。本节将详细分析解析过程。 元素parameterMap将会解析为ParameterMap对象,该对象包含...

祖大俊
2016/05/04
851
0
Mybatis3.3.x技术内幕(一):SqlSession和SqlSessionFactory列传

前言:我长大了,成年了,有需求,但我单身,所以我要讨个媳妇,要求是:漂亮、高挑、身材好、笑容甜美…… 和A相亲:漂亮,不够高挑。 和B相亲:高挑,身材不够好。 和C相亲:身材好,笑容不...

祖大俊
2016/04/25
3.3K
2
Mybatis3.3.x技术内幕(十五):Mybatis之foreach批量insert,返回主键id列表(修复Mybatis返回null的bug)

Mybatis在执行批量插入时,如果使用的是for循环逐一插入,那么可以正确返回主键id。如果使用动态sql的foreach循环,那么返回的主键id列表,可能为null,这让很多人感到困惑;本文将分析问题产...

祖大俊
2016/05/13
9.6K
17
Mybatis3.3.x技术内幕(十二):Mybatis之TypeHandler

Mybatis中的TypeHandler有两个功能,一个是完成javaType至jdbcType的转换,另外一个是完成jdbcType至javaType的转换。 public interface TypeHandler<T> { void setParameter(PreparedStatem......

祖大俊
2016/05/06
991
0
Mybatis3.3.x技术内幕(八):Mybatis初始化流程(上)

Mybatis初始化流程,其实就是组装重量级All-In-One对象Configuration的过程,主要分为系统环境参数初始化和Mapper映射初始化,其中Mapper映射初始化尤为重要。 inputStream = Resources.getR...

祖大俊
2016/05/02
1K
2

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Ubuntu18.04 显卡GF-940MX安装NVIDIA-390.77

解决办法: 下面就给大家一个正确的姿势在Ubuntu上安装Nvidia驱动: (a)首先去N卡官网下载自己显卡对应的驱动:www.geforce.cn/drivers (b)下载后好放在英文路径的目录下,怎么简单怎么来...

AI_SKI
今天
0
0
深夜胡思乱想

魔兽世界 最近魔兽世界出了新版本, 周末两天升到了满级,比之前的版本体验好很多,做任务不用抢怪了,不用组队打怪也是共享拾取的。技能简化了很多,哪个亮按哪个。 运维 服务器 产品 之间的...

Firxiao
今天
0
0
MySQL 8 在 Windows 下安装及使用

MySQL 8 带来了全新的体验,比如支持 NoSQL、JSON 等,拥有比 MySQL 5.7 两倍以上的性能提升。本文讲解如何在 Windows 下安装 MySQL 8,以及基本的 MySQL 用法。 下载 下载地址 https://dev....

waylau
今天
0
0
微信第三方平台 access_token is invalid or not latest

微信第三方开发平台code换session_key说的特别容易,但是我一使用就带来无穷无尽的烦恼,搞了一整天也无济于事. 现在记录一下解决问题的过程,方便后来人参考. 我遇到的这个问题搜索了整个网络也...

自由的开源
今天
2
0
openJDK之sun.misc.Unsafe类CAS底层实现

注:这篇文章参考了https://www.cnblogs.com/snowater/p/8303698.html 1.sun.misc.Unsafe中CAS方法 在sun.misc.Unsafe中CAS方法如下: compareAndSwapObject(java.lang.Object arg0, long a......

汉斯-冯-拉特
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部