輕量級封裝DbUtils&Mybatis之四MyBatis主鍵

MyBatis主鍵

不支持對象列表存儲時對自增id字段的賦值(至少包括3.2.6和3.3.0版本),若是id不是採用底層DB自增主鍵賦值,沒必要考慮此問題
舒適提示:分佈式DB環境下,DB主鍵通常會採用統一的Id生成器生成Id,所以沒必要考慮由數據庫自增策略填充主鍵值。html

解決方案

參考源碼

1)mybatis-batch-insert項目,請爲原做者點贊,支持他開源
備註:實際代碼有少許修改,會在下文列出,本文依據實現方案代碼細節反推分析源碼處理邏輯過程java

批量插入對象列表自增主鍵賦值分析

1)在獲取數據庫返回的主鍵值後填充到中間存儲結構。
2)在構造具體返回對象結構過程當中(其實insert語句並不須要),從中間存儲結構將多個主鍵值填充到具體的對象實例當中。git

備註:實際上這種解決方案仍是來源於代碼分析的結果,接下來簡單列述Mybatis主鍵處理的核心代碼及配置github

MyBatis處理主鍵處理流程

MyBatis自動生成主鍵插入記錄時序圖

備註:主鍵填充的處理方法實際是populateKeys。spring

代碼呈上

測試示例sql

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]

模型HomeTownapache

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());
        }
    }
}

HomeTownDaobash

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配置文件mybatis

<?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

太累了,這一篇。。。

相關文章
相關標籤/搜索