• 轻量级封装DbUtils&Mybatis之四MyBatis主键


    MyBatis主键#

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

    解决方案#

    参考源码##

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

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

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

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

    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 {
    
        @Autowired
        private HomeTownDao homeTownDao;
    
        @Test
        public 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);
        }
    
        @Override
        public 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 mapper
        PUBLIC "-//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.xsd
         http://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);
        }
    
        @Override
        public void setNonNullParameter(PreparedStatement ps, int i, BatchInsertEntityPrimaryKeys parameter,
                JdbcType jdbcType) throws SQLException {
            // TODO Auto-generated method stub
            //System.out.println(" BatchInsertEntitiesTypeHandler#setNonNullParameter got called. ");
        }
    
        @Override
        public BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, String columnName) throws SQLException {
            // TODO Auto-generated method stub
            //System.out.println(" BatchInsertEntitiesTypeHandler#getNullableResult got called. ");
            return null;
        }
    
        @Override
        public 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;
        }
    
        @Override
        public 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});
                }
            }
        }
    
        @Override
        public 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;
        }
    
        @Override
        public String findProperty(String name, boolean useCamelCaseMapping) {
            return null;
        }
    
        @Override
        public String[] getGetterNames() {
            return null;
        }
    
        @Override
        public 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)
         */
        @Override
        public 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;
        }
    
        @Override
        public Class<?> getGetterType(String name) {
            return null;
        }
    
        @Override
        public 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;
        }
    
        @Override
        public boolean hasGetter(String name) {
            return false;
        }
    
        @Override
        public MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory) {
            return null;
        }
    
        @Override
        public boolean isCollection() {
            return false;
        }
    
        @Override
        public void add(Object element) {
    
        }
    
        @Override
        public <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.MetaObject
            metaParam.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.BatchInsertObjectWrapper
          objectWrapper.set(prop, value);
        }
      }
    
    

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

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

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

        @Override
        public 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#

    太累了,这一篇。。。

  • 相关阅读:
    Spring cloud学习总结
    Spring boot学习笔记
    Rabbitmq安装步骤
    Mongodb 笔记采坑
    Rabbit Docker 部署及采坑
    各种知识汇总
    Echart 随便写的
    Linux常用命令
    Redis学习笔记
    Docker使用总结
  • 原文地址:https://www.cnblogs.com/fangfan/p/4994802.html
Copyright © 2020-2023  润新知