不支持對象列表存儲時對自增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
備註:主鍵填充的處理方法實際是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); } }
此事要回到源碼當中找答案。
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
2)因爲1)中自定義了存儲結構(BatchInsertEntities)須要處理主鍵,所以須要定義一個新的主鍵類型BatchInsertEntityPrimaryKeys 並綁定一個TypeHandler才能夠處理此類型
3) ObjectWrapper和TypeHandler其實是相輔相成的關係,有了類型處理器將ResultSet中的主鍵數據轉換爲目標對象可接受的類型,那麼填充目標對象主鍵的工做就由ObjectWrapper來完成了,它們是相互協做的關係。
若是仍然對上述過程有疑問,請務必調試代碼,做者不太聰明,調試了n次纔讀懂了完整過程。
最後給個提示,一個普通Bean是如何填充主鍵的,請查看org.apache.ibatis.reflection.wrapper.BeanWrapper及org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator,想必必有收穫。
太累了,這一篇。。。