Skip to content

Commit

Permalink
Merge pull request mybatis#1249 from harawata/multi-row-insert-improv…
Browse files Browse the repository at this point in the history
…ement-2

Remove parameter name restriction when assigning keys generated by multi-row insert.
  • Loading branch information
harawata authored Aug 16, 2018
2 parents d4d420e + d393fab commit 37c25ee
Show file tree
Hide file tree
Showing 6 changed files with 642 additions and 53 deletions.
125 changes: 90 additions & 35 deletions src/main/java/org/apache/ibatis/executor/keygen/Jdbc3KeyGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.ArrayUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
Expand All @@ -42,6 +43,7 @@ public class Jdbc3KeyGenerator implements KeyGenerator {

/**
* A shared instance.
*
* @since 3.4.3
*/
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
Expand All @@ -53,29 +55,24 @@ public void processBefore(Executor executor, MappedStatement ms, Statement stmt,

@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, getParameters(parameter));
processBatch(ms, stmt, parameter);
}

public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
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) {
// 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);
if (rs.getMetaData().getColumnCount() >= keyProperties.length) {
Object soleParam = getSoleParameter(parameter);
if (soleParam != null) {
assignKeysToParam(configuration, rs, keyProperties, soleParam);
} else {
assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter);
}
}
} catch (Exception e) {
Expand All @@ -91,25 +88,83 @@ public void processBatch(MappedStatement ms, Statement stmt, Collection<Object>
}
}

private Collection<Object> getParameters(Object parameter) {
Collection<Object> parameters = null;
if (parameter instanceof Collection) {
parameters = (Collection) parameter;
} else if (parameter instanceof Map) {
Map parameterMap = (Map) parameter;
if (parameterMap.containsKey("collection")) {
parameters = (Collection) parameterMap.get("collection");
} else if (parameterMap.containsKey("list")) {
parameters = (List) parameterMap.get("list");
} else if (parameterMap.containsKey("array")) {
parameters = Arrays.asList((Object[]) parameterMap.get("array"));
protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties,
Map<?, ?> paramMap) throws SQLException {
// Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'.
int firstDot = keyProperties[0].indexOf('.');
if (firstDot == -1) {
throw new ExecutorException(
"Could not determine which parameter to assign generated keys to. "
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
+ paramMap.keySet());
}
String paramName = keyProperties[0].substring(0, firstDot);
Object param;
if (paramMap.containsKey(paramName)) {
param = paramMap.get(paramName);
} else {
throw new ExecutorException("Could not find parameter '" + paramName + "'. "
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
+ paramMap.keySet());
}
// Remove param name from 'keyProperty' string. e.g. 'param.id' -> 'id'
String[] modifiedKeyProperties = new String[keyProperties.length];
for (int i = 0; i < keyProperties.length; i++) {
if (keyProperties[i].charAt(firstDot) == '.' && keyProperties[i].startsWith(paramName)) {
modifiedKeyProperties[i] = keyProperties[i].substring(firstDot + 1);
} else {
throw new ExecutorException("Assigning generated keys to multiple parameters is not supported. "
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
+ paramMap.keySet());
}
}
if (parameters == null) {
parameters = new ArrayList<>();
parameters.add(parameter);
assignKeysToParam(configuration, rs, modifiedKeyProperties, param);
}

private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties,
Object param)
throws SQLException {
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
final ResultSetMetaData rsmd = rs.getMetaData();
// Wrap the parameter in Collection to normalize the logic.
Collection<?> paramAsCollection = null;
if (param instanceof Object[]) {
paramAsCollection = Arrays.asList((Object[]) param);
} else if (!(param instanceof Collection)) {
paramAsCollection = Arrays.asList(param);
} else {
paramAsCollection = (Collection<?>) param;
}
TypeHandler<?>[] typeHandlers = null;
for (Object obj : paramAsCollection) {
if (!rs.next()) {
break;
}
MetaObject metaParam = configuration.newMetaObject(obj);
if (typeHandlers == null) {
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
}
populateKeys(rs, metaParam, keyProperties, typeHandlers);
}
}

private Object getSoleParameter(Object parameter) {
if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) {
return parameter;
}
Object soleParam = null;
for (Object paramValue : ((Map<?, ?>) parameter).values()) {
if (soleParam == null) {
soleParam = paramValue;
} else if (soleParam != paramValue) {
soleParam = null;
break;
}
}
return parameters;
return soleParam;
}

private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2009-2016 the original author or authors.
* Copyright 2009-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,10 +16,75 @@
package org.apache.ibatis.submitted.keygen;

import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;

public interface CountryMapper {

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert({ "insert into country (countryname,countrycode) values (#{countryname},#{countrycode})" })
int insertBean(Country country);

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert({ "insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})" })
int insertNamedBean(@Param("country") Country country);

int insertList(List<Country> countries);

int insertNamedList(@Param("countries") List<Country> countries);

int insertSet(Set<Country> countries);

int insertNamedSet(@Param("countries") Set<Country> countries);

int insertArray(Country[] countries);

int insertNamedArray(@Param("countries") Country[] countries);

@Options(useGeneratedKeys = true, keyProperty = "country.id")
@Insert({ "insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})" })
int insertMultiParams(@Param("country") Country country, @Param("someId") Integer someId);

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert({ "insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})" })
int insertMultiParams_keyPropertyWithoutParamName(@Param("country") Country country, @Param("someId") Integer someId);

@Options(useGeneratedKeys = true, keyProperty = "bogus.id")
@Insert({ "insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})" })
int insertMultiParams_keyPropertyWithWrongParamName(@Param("country") Country country,
@Param("someId") Integer someId);

int insertListAndSomeId(@Param("list") List<Country> countries, @Param("someId") Integer someId);

int insertSetAndSomeId(@Param("collection") Set<Country> countries, @Param("someId") Integer someId);

int insertArrayAndSomeId(@Param("array") Country[] countries, @Param("someId") Integer someId);

int insertList_MultiParams(@Param("countries") List<Country> countries, @Param("someId") Integer someId);

int insertSet_MultiParams(@Param("countries") Set<Country> countries, @Param("someId") Integer someId);

int insertArray_MultiParams(@Param("countries") Country[] countries, @Param("someId") Integer someId);

int insertUndefineKeyProperty(Country country);

@Options(useGeneratedKeys = true, keyProperty = "id,code")
@Insert({ "insert into planet (name) values (#{name})" })
int insertPlanet(Planet planet);

int insertPlanets(List<Planet> planets);

@Options(useGeneratedKeys = true, keyProperty = "planet.id,planet.code")
@Insert({ "insert into planet (name) values (#{planet.name})" })
int insertPlanet_MultiParams(@Param("planet") Planet planet, @Param("someId") Integer someId);

int insertPlanets_MultiParams(@Param("planets") List<Planet> planets, @Param("someId") Integer someId);

@Options(useGeneratedKeys = true, keyProperty = "planet.id,map.code")
@Insert({ "insert into planet (name) values (#{planet.name})" })
int insertAssignKeysToTwoParams(@Param("planet") Planet planet, @Param("map") Map<String, Object> map);
}
124 changes: 113 additions & 11 deletions src/test/java/org/apache/ibatis/submitted/keygen/CountryMapper.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2009-2016 the original author or authors.
Copyright 2009-2018 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -19,15 +19,117 @@
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.submitted.keygen.CountryMapper" >
<insert id="insertList" parameterType="org.apache.ibatis.submitted.keygen.Country" useGeneratedKeys="true" keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="list" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertUndefineKeyProperty" parameterType="org.apache.ibatis.submitted.keygen.Country" useGeneratedKeys="true" keyProperty="country_id">
insert into country (countryname,countrycode) values (#{countryname},#{countrycode})
<mapper namespace="org.apache.ibatis.submitted.keygen.CountryMapper">
<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="list" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertNamedList" useGeneratedKeys="true"
keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="countries" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertSet" useGeneratedKeys="true" keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="collection" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertNamedSet" useGeneratedKeys="true" keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="countries" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertArray" useGeneratedKeys="true" keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="array" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertNamedArray" useGeneratedKeys="true" keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="countries" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertListAndSomeId" useGeneratedKeys="true"
keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="list" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertSetAndSomeId" useGeneratedKeys="true"
keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="collection" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertArrayAndSomeId" useGeneratedKeys="true"
keyProperty="id">
insert into country (countryname,countrycode)
values
<foreach collection="array" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertList_MultiParams" useGeneratedKeys="true"
keyProperty="countries.id">
insert into country (countryname,countrycode)
values
<foreach collection="countries" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertSet_MultiParams" useGeneratedKeys="true"
keyProperty="countries.id">
insert into country (countryname,countrycode)
values
<foreach collection="countries" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertArray_MultiParams" useGeneratedKeys="true"
keyProperty="countries.id">
insert into country (countryname,countrycode)
values
<foreach collection="countries" separator="," item="country">
(#{country.countryname},#{country.countrycode})
</foreach>
</insert>
<insert id="insertUndefineKeyProperty" useGeneratedKeys="true"
keyProperty="country_id">
insert into country (countryname,countrycode) values
(#{countryname},#{countrycode})
</insert>
<insert id="insertPlanets" useGeneratedKeys="true" keyProperty="id,code">
insert into planet (name) values
<foreach collection="list" separator="," item="planet">
(#{planet.name})
</foreach>
</insert>
<insert id="insertPlanets_MultiParams" useGeneratedKeys="true" keyProperty="planets.id,planets.code">
insert into planet (name) values
<foreach collection="planets" separator="," item="planet">
(#{planet.name})
</foreach>
</insert>
<insert id="insertPlanetAndCountry" useGeneratedKeys="true" keyProperty="planet.id,planet.code,country.id">
insert into planet (name) values (#{planet.name});
insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode});
</insert>
</mapper>
Loading

0 comments on commit 37c25ee

Please sign in to comment.