MyBatis主键

不支持对象列表存储时对自增id字段的赋值(至少包括3.2.6和3.3.0版本),如果id不是采用底层DB自增主键赋值,不必考虑此问题
温馨提示:分布式DB环境下,DB主键一般会采用统一的Id生成器生成Id,因此不必考虑由数据库自增策略填充主键值。

解决方案

参考源码

1)mybatis-batch-insert项目,请为原作者点赞,支持他开源
备注:实际代码有少量修改,会在下文列出,本文依据实现方案代码细节反推分析源码处理逻辑过程

批量插入对象列表自增主键赋值分析

1)在获取数据库返回的主键值后填充到中间存储结构。
2)在构造具体返回对象结构过程中(其实insert语句并不需要),从中间存储结构将多个主键值填充到具体的对象实例当中。

备注:实际上这种解决方案还是来源于代码分析的结果,接下来简单列述Mybatis主键处理的核心代码及配置

MyBatis处理主键处理流程

备注:主键填充的处理方法实际是populateKeys。

代码呈上

测试示例

package org.wit.ff.jdbc;import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.wit.ff.jdbc.dao.HomeTownDao;
import org.wit.ff.jdbc.id.BatchInsertEntities;
import org.wit.ff.jdbc.model.HomeTown;
import org.wit.ff.jdbc.query.Criteria;import java.util.ArrayList;
import java.util.List;/*** Created by F.Fang on 2015/11/17.* Version :2015/11/17*/
@ContextConfiguration(locations = {"classpath:applicationContext-batch.xml"})
public class HomeTownDaoBatchTest extends AbstractJUnit4SpringContextTests {@Autowiredprivate HomeTownDao homeTownDao;@Testpublic void testBatchInsert(){HomeTown ht1 = new HomeTown();ht1.setName("hb");ht1.setLocation("hubei");HomeTown ht2 = new HomeTown();ht2.setName("js");ht2.setLocation("jiangsu");List<HomeTown> list = new ArrayList<>();list.add(ht1);list.add(ht2);BatchInsertEntities<HomeTown> batchEntities = new BatchInsertEntities<>(list);homeTownDao.batchInsert(batchEntities);System.out.println(batchEntities.getEntities());}}

控制台输出

[3,hb,hubei, 4,js,jiangsu]

模型HomeTown

package org.wit.ff.jdbc.model;import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.wit.ff.jdbc.id.IdGenerator;/*** Created by F.Fang on 2015/11/17.* Version :2015/11/17*/
public class HomeTown implements IdGenerator {private int id;private String name;private String location;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getLocation() {return location;}public void setLocation(String location) {this.location = location;}public String toString() {return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);}@Overridepublic void parseGenKey(Object[] value) {if(value!=null && value.length == 1){this.id = Integer.valueOf(value[0].toString());}}
}

HomeTownDao

package org.wit.ff.jdbc.dao;import org.wit.ff.jdbc.id.BatchInsertEntities;
import org.wit.ff.jdbc.model.HomeTown;import java.util.List;/*** Created by F.Fang on 2015/11/17.* Version :2015/11/17*/
public interface HomeTownDao {void batchInsert(BatchInsertEntities<HomeTown> batchEntities);
}

Mapper配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wit.ff.jdbc.dao.HomeTownDao"><insert id="batchInsert" parameterType="org.wit.ff.jdbc.id.BatchInsertEntities" useGeneratedKeys="true" keyProperty="id"keyColumn="ID">insert into hometown(name,location)values<foreach item="item" collection="entities" separator=",">( #{item.name},#{item.location})</foreach></insert></mapper>

Spring配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd"><!-- 数据源 --><bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close"><property name="driverClassName" value="${db.driverClass}"/><property name="url" value="${db.jdbcUrl}"/><property name="username" value="${db.user}"/><property name="password" value="${db.password}"/></bean><!-- 配置 SqlSessionFactory --><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource"/><!-- 制定路径自动加载mapper配置文件 --><property name="mapperLocations" value="classpath:mappers/*Dao.xml"/><!-- 配置myibatis的settings http://mybatis.github.io/mybatis-3/zh/configuration.html#settings --><property name="configurationProperties"><props><prop key="cacheEnabled">true</prop></props></property><property name="typeHandlers"><list><bean class="org.wit.ff.jdbc.id.BatchInsertEntitiesTypeHandler"/></list></property><property name="objectWrapperFactory" ref="batchObjectWrapperFactory"/><!-- 类型别名是为 Java 类型命名一个短的名字。 它只和 XML 配置有关, 只用来减少类完全 限定名的多余部分 --><property name="typeAliasesPackage" value="org.wit.ff.jdbc.model"/></bean><bean id="batchObjectWrapperFactory" class="org.wit.ff.jdbc.id.BatchInsertObjectWrapperFactory"/><mybatis:scan base-package="org.wit.ff.jdbc.dao"/></beans>

存储主键值的结构

package org.wit.ff.jdbc.id;import java.util.List;public class BatchInsertEntityPrimaryKeys {private final List<String> primaryKeys;public BatchInsertEntityPrimaryKeys(List<String> pks) {this.primaryKeys = pks;}public List<String> getPrimaryKeys() {return primaryKeys;}
}

批量对象列表包装

package org.wit.ff.jdbc.id;import java.util.List;public class BatchInsertEntities<T extends IdGenerator> {private final List<T> entities;public BatchInsertEntities(List<T> entities) {this.entities = entities;}/*** <p>* The entities will be batch inserted into DB. The entities are also the* parameters of the* {@link org.apache.ibatis.binding.MapperMethod.SqlCommand}.*/public List<T> getEntities() {return entities;}
}

自定义TypeHandler

package org.wit.ff.jdbc.id;import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;public class BatchInsertEntitiesTypeHandler extends BaseTypeHandler<BatchInsertEntityPrimaryKeys> {public BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, int columnIndex) throws SQLException {// Read the primary key values from result set. It is believed that// there is 1 primary key column.List<String> pks = new LinkedList<>();do {// rs.next is called before.pks.add(rs.getString(columnIndex));} while (rs.next());return new BatchInsertEntityPrimaryKeys(pks);}@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, BatchInsertEntityPrimaryKeys parameter,JdbcType jdbcType) throws SQLException {// TODO Auto-generated method stub//System.out.println(" BatchInsertEntitiesTypeHandler#setNonNullParameter got called. ");}@Overridepublic BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, String columnName) throws SQLException {// TODO Auto-generated method stub//System.out.println(" BatchInsertEntitiesTypeHandler#getNullableResult got called. ");return null;}@Overridepublic BatchInsertEntityPrimaryKeys getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {// TODO Auto-generated method stub//System.out.println(" BatchInsertEntitiesTypeHandler#getNullableResult got called. ");return null;}}

自定义ObjectWrapper

package org.wit.ff.jdbc.id;import java.util.Iterator;
import java.util.List;import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;/*** Wrap the collection object for batch insert.* https://github.com/jactive/java*/
public class BatchInsertObjectWrapper implements ObjectWrapper {private final BatchInsertEntities<IdGenerator> entity;public BatchInsertObjectWrapper(MetaObject metaObject, BatchInsertEntities<IdGenerator> object) {this.entity = object;}@Overridepublic void set(PropertyTokenizer prop, Object value) {// check the primary key type existed or not when setting PK by reflection.BatchInsertEntityPrimaryKeys pks = (BatchInsertEntityPrimaryKeys) value;if (pks.getPrimaryKeys().size() == entity.getEntities().size()) {Iterator<String> iterPks = pks.getPrimaryKeys().iterator();Iterator<IdGenerator> iterEntities = entity.getEntities().iterator();while (iterPks.hasNext()) {String id = iterPks.next();IdGenerator entity = iterEntities.next();//System.out.println(id + "|" + entity);entity.parseGenKey(new Object[]{id});}}}@Overridepublic Object get(PropertyTokenizer prop) {// Only the entities or parameters property of BatchInsertEntities// can be accessed by mapper.// 这一段是决定最终返回数据结果.if ("entities".equals(prop.getName()) ||"parameters".equals(prop.getName())) {return entity.getEntities();}return null;}@Overridepublic String findProperty(String name, boolean useCamelCaseMapping) {return null;}@Overridepublic String[] getGetterNames() {return null;}@Overridepublic String[] getSetterNames() {return null;}/*** 此函数返回类型和BatchInsertEntitiesTypeHandler的泛型类型一致.* Jdbc3KeyGenerator.* Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);* TypeHandler<?> th = typeHandlerRegistry.getTypeHandler(keyPropertyType);** @param name* @return* @see org.apache.ibatis.reflection.wrapper.ObjectWrapper#getSetterType(java.lang.String)*/@Overridepublic Class<?> getSetterType(String name) {// Return the primary key setter type.// Here, we return the BatchInsertEntityPrimaryKeys because// there are several primary keys  in the result set of// INSERT statement.return BatchInsertEntityPrimaryKeys.class;}@Overridepublic Class<?> getGetterType(String name) {return null;}@Overridepublic boolean hasSetter(String name) {// In BatchInsertObjectWrapper, name is the primary key property name.// Always return true here without checking if there is such property// in BatchInsertEntities#getEntities().get(0) . The verification be// postphone until setting the PK value at this.set method.return true;}@Overridepublic boolean hasGetter(String name) {return false;}@Overridepublic MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory) {return null;}@Overridepublic boolean isCollection() {return false;}@Overridepublic void add(Object element) {}@Overridepublic <E> void addAll(List<E> element) {}
}

自定义ObjectWrapperFactory

package org.wit.ff.jdbc.id;import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;public class BatchInsertObjectWrapperFactory implements ObjectWrapperFactory {public boolean hasWrapperFor(Object object) {return null != object && BatchInsertEntities.class.isAssignableFrom(object.getClass());}public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {return new BatchInsertObjectWrapper(metaObject, (BatchInsertEntities<IdGenerator>)object);}}

源代码分析

  • 为什么定义一个BatchInsertEntities而不直接使用List
  • 自定义TypeHandler的目的
  • 自定义ObjectWrapper(factory)

此事要回到源码当中找答案。

1)上文的时序图定位主键核心处理代码起始方法:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.populateAfter

注意mapper配置文件配置 useGeneratedKeys="true" keyProperty="id" keyColumn="ID"

public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {List<Object> parameters = new ArrayList<Object>();parameters.add(parameter);processBatch(ms, stmt, parameters);}public void processBatch(MappedStatement ms, Statement stmt, List<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) {for (Object parameter : parameters) {if (!rs.next()) break; // there should be one row for each statement (also one for each parameter)final MetaObject metaParam = configuration.newMetaObject(parameter);// 1,找typeHandlers的逻辑为关键.if (typeHandlers == null) typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties);// 2, 填充键值.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}}}}

步骤1 查找TypeHandler

BatchInsertEntitiesTypeHandler负责处理BatchInsertEntityPrimaryKeys类型,并定义了getNull(rs,int index)方法,后面可以看到这个方法在主键填充时被调用.

private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties) {TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];for (int i = 0; i < keyProperties.length; i++) {if (metaParam.hasSetter(keyProperties[i])) {// metaParam getSetterType --> BatchInsertObjectWrapper定义了getSetterType,参考MetaObject中获取getSetterType的源码,实际是从自定义的ObjectWrapper中获取Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);// 从spring xml 配置中找TypeHandler的配置.TypeHandler<?> th = typeHandlerRegistry.getTypeHandler(keyPropertyType);typeHandlers[i] = th;}}return typeHandlers;}

小结:获取TypeHandler的过程实际是依据ObjectWrapper指定的SetterType拿到KeyPropertyType(主键类型),再通过主键类型从用户配置的SessionFactory当中获取(上文SessionFactory中配置BatchInsertEntitiesTypeHandler)

步骤2 填充主键

请参考BaseTypehandler中的getResult方法,实际调用了getNullResult方法,此方法BatchInsertEntitiesTypeHandler已有实现,经过调试发现它依据配置的主键名称"id"从resultset中获取了一列id值。

  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) {// 即调用BatchInsertEntitiesTypeHandler的public BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, int columnIndex)的方法// 将主键值记录在BatchInsertEntityPrimaryKeys的对象当中,此时value的类型是BatchInsertEntityPrimaryKeys.Object value = th.getResult(rs, i + 1);// 调用  org.apache.ibatis.reflection.MetaObjectmetaParam.setValue(keyProperties[i], value);}}}public void setValue(String name, Object value) {// 被设置的属性是不含有"." , 目前是id, 而不是(item.id)这样的字符串 因此会执行else.PropertyTokenizer prop = new PropertyTokenizer(name);if (prop.hasNext()) {MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());if (metaValue == SystemMetaObject.NULL_META_OBJECT) {if (value == null && prop.getChildren() != null) {return; // don't instantiate child path if value is null} else {metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);}}metaValue.setValue(prop.getChildren(), value);} else {// 核心执行逻辑.// 此处的objectWrapper 对应我们自定义的org.wit.ff.BatchInsertObjectWrapperobjectWrapper.set(prop, value);}}

小结:依据目标TypeHandler,调用getNullResult处理主键,从ResultSet当中拿到一列主键值,并包装成BatchInsertEntityPrimaryKeys返回,作为参数执行目标ObjectWrapper的set方法,请参考上文BatchInsertObjectWrapper。

至此,从ResultSet返回的主键值列表已经被我们自定义的ObjectWrapper截获。

步骤3 BatchInsertObjectWrapper填充主键值到原始对象列表
HowTown类型实现了IdGenerator接口, 调用parseGenKey即可填充主键到目标对象上,详情参考上文的HownTown源码。

    @Overridepublic void set(PropertyTokenizer prop, Object value) {// check the primary key type existed or not when setting PK by reflection.BatchInsertEntityPrimaryKeys pks = (BatchInsertEntityPrimaryKeys) value;if (pks.getPrimaryKeys().size() == entity.getEntities().size()) {Iterator<String> iterPks = pks.getPrimaryKeys().iterator();Iterator<IdGenerator> iterEntities = entity.getEntities().iterator();while (iterPks.hasNext()) {String id = iterPks.next();IdGenerator entity = iterEntities.next();//System.out.println(id + "|" + entity);entity.parseGenKey(new Object[]{id});}}}

三个问题的答案

1)自定义存储对象列表的结构的原因在于MyBatis处理主键时始终将对象作为"一个"来看待,并且要绑定主键类型,而List是集合类型,类型是List
2)由于1)中自定义了存储结构(BatchInsertEntities)需要处理主键,因此需要定义一个新的主键类型BatchInsertEntityPrimaryKeys 并绑定一个TypeHandler才可以处理此类型
3) ObjectWrapper和TypeHandler实际上是相辅相成的关系,有了类型处理器将ResultSet中的主键数据转换为目标对象可接受的类型,那么填充目标对象主键的工作就由ObjectWrapper来完成了,它们是相互协作的关系。

总结

如果仍然对上述过程有疑问,请务必调试代码,作者不太聪明,调试了n次才读懂了完整过程。
最后给个提示,一个普通Bean是如何填充主键的,请查看org.apache.ibatis.reflection.wrapper.BeanWrapper及org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator,想必必有收获。

QA

太累了,这一篇。。。

转载于:https://www.cnblogs.com/fangfan/p/4994802.html

轻量级封装DbUtilsMybatis之四MyBatis主键相关推荐

  1. MyBatis主键回填和自定义主键

    MyBatis主键回填和自定义主键 1. 主键回填 JDBC中的Statement对象在执行插入的SQL后,可以通过getGeneratedKeys方法获得数据库生成的主键,这样便能达到获取主键的功能 ...

  2. mybatis主键生成策略和mp主键生成策略

    mybatis主键生成策略和mp主键生成策略 1,mybatis plus 主键生成策略 都是通过给实体类的属性添加注解的方式执行type = IDTYPE- ​ 1,AUTO数据库ID自增 ​ 2, ...

  3. mybatis 主键自增 insert后返回主键

    mybatis 主键自增 insert后返回主键 : <insert id="insertStudentAutoKey" parameterType="Studen ...

  4. Mybatis 主键回显 KeyGenerator原理

    这篇文章研究下 Mybatis 配置主键回显相关功能. 本篇文章将以以下几个问题切入: Mybatis 如何 配置主键自增回显? JDBC 主键回显用法? 对于不支持自增主键数据库,Mybatis 有 ...

  5. @MyBatis主键返回

    在使用MyBatis做持久层时,insert语句默认是不返回记录的主键值,而是返回插入的记录条数:如果业务层需要得到记录的主键时,可以通过配置的方式来完成这个功能. 比如在表的关联关系中,将数据插入主 ...

  6. mysql mybatis 主键id_MyBatis+MySQL 返回插入的主键ID-Go语言中文社区

    需求:使用MyBatis往MySQL数据库中插入一条记录后,需要返回该条记录的自增主键值. 方法:在mapper中指定keyProperty属性,示例如下: insert into user(user ...

  7. mysql mybatis 主键id_MyBatis+MySQL 返回插入的主键ID

    需求:使用MyBatis往MySQL数据库中插入一条记录后,需要返回该条记录的自增主键值. 方法一 在mapper中指定keyProperty属性,示例如下: insert into user(use ...

  8. Mybatis—— 主键回填

    在 MySQL 中主键自增字段,在插入后往往需要获得这个主键,以便于后面的操作,而 MyBatis 提供了实现的方法. 首先可以使用 keyProperty 属性指定哪个是主键字段,同时使用 useG ...

  9. mybatis主键返回

    自增主键的返回: mysql自增主键,执行insert提交之前自动生成一个自增主键. 通过mysql函数获取到刚插入记录的自增主键: LAST_INSERT_ID() 是insert之后调用此函数. ...

最新文章

  1. Centos7安装编译安装zabbix2.219及mariadb-5.5.46
  2. OpenStack 虚拟机的磁盘文件类型与存储方式
  3. 最长递增子序列 最长连续递增序列
  4. 【PAT乙级】1058 选择题 (20 分)
  5. 【spring boot】 mybatis配置双数据源/多数据源
  6. 【Android】手机端的投射
  7. 计算机符号的英文名,【常见符号英文名称】英文名称
  8. linux下能用qt5.0,qt5.0移植
  9. Hey, everybody!
  10. 前端学习(1897)vue之电商管理系统电商系统之实现搜索功能
  11. mysql57数据库命令_MySQL 5.7 mysql command line client 使用命令详解
  12. 课工场新闻管理jsp修改项目_jspmvc实验室预约管理系统
  13. TensorFlow使用--MNIST分类学习(BP神经网络)
  14. R语言在大气污染数据分析中的应用-时间序列分析(一)
  15. AOP:静态代理实现方式①通过继承②通过接口
  16. 2020 macbook pro 16寸 前端开发 我的装机软件整理
  17. 10 个超棒的 jQuery 视频插件
  18. Comma Separated Values Format
  19. ai怎么调界面大小_Adobe Illustrator(Ai)怎么改变页面大小,快捷键是什么?
  20. 使用GCC和Makefile编译c文件

热门文章

  1. 微软2014校园招聘笔试编程题
  2. java 新浪短链接_新浪t.cn短链接如何生成?网址缩短api接口分享
  3. 敏捷团队的病与药——阿里健康医药B2B团队敏捷转型手记
  4. 正睿20秋季普转提day3
  5. 他曾被视为马斯克第二,现在是等着坐牢的骗子
  6. 爬虫之模拟强智系统登录
  7. JS复习笔记之造new轮子
  8. One Drive 回收站文件太多时无法清空回收站解决办法
  9. eclipse新建java项目报错 jrt-fs.jar
  10. HDU6069(数学)