Skip to content

Commit 36d3a4a

Browse files
committed
fixes #1485 Improved consistency in assigning generated keys when using BatchExecutor.
Jdbc3KeyGenerator is fully rewritten and it now supports assigning generated keys to different parameters. See the test 'assigningMultipleKeysToDifferentParams'.
1 parent 4dfea24 commit 36d3a4a

File tree

3 files changed

+289
-96
lines changed

3 files changed

+289
-96
lines changed

src/main/java/org/apache/ibatis/executor/keygen/Jdbc3KeyGenerator.java

Lines changed: 163 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
import java.sql.ResultSetMetaData;
2020
import java.sql.SQLException;
2121
import java.sql.Statement;
22+
import java.util.AbstractMap;
23+
import java.util.ArrayList;
2224
import java.util.Arrays;
2325
import java.util.Collection;
26+
import java.util.HashMap;
27+
import java.util.Iterator;
28+
import java.util.List;
2429
import java.util.Map;
30+
import java.util.Map.Entry;
2531

2632
import org.apache.ibatis.binding.MapperMethod.ParamMap;
2733
import org.apache.ibatis.executor.Executor;
@@ -64,122 +70,190 @@ public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
6470
return;
6571
}
6672
try (ResultSet rs = stmt.getGeneratedKeys()) {
73+
final ResultSetMetaData rsmd = rs.getMetaData();
6774
final Configuration configuration = ms.getConfiguration();
68-
if (rs.getMetaData().getColumnCount() >= keyProperties.length) {
69-
Object soleParam = getSoleParameter(parameter);
70-
if (soleParam != null) {
71-
assignKeysToParam(configuration, rs, keyProperties, soleParam);
72-
} else {
73-
assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter);
74-
}
75+
if (rsmd.getColumnCount() < keyProperties.length) {
76+
// Error?
77+
} else {
78+
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
7579
}
7680
} catch (Exception e) {
7781
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
7882
}
7983
}
8084

81-
protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties,
82-
Map<?, ?> paramMap) throws SQLException {
83-
// Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'.
84-
int firstDot = keyProperties[0].indexOf('.');
85-
if (firstDot == -1) {
86-
throw new ExecutorException(
87-
"Could not determine which parameter to assign generated keys to. "
88-
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
89-
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
90-
+ paramMap.keySet());
91-
}
92-
String paramName = keyProperties[0].substring(0, firstDot);
93-
Object param;
94-
if (paramMap.containsKey(paramName)) {
95-
param = paramMap.get(paramName);
85+
@SuppressWarnings("unchecked")
86+
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
87+
Object parameter) throws SQLException {
88+
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
89+
// Multi-param or single param with @Param
90+
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
91+
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
92+
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
93+
// Multi-param or single param with @Param in batch operation
94+
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, ((ArrayList<ParamMap<?>>) parameter));
9695
} else {
97-
throw new ExecutorException("Could not find parameter '" + paramName + "'. "
98-
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
99-
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
100-
+ paramMap.keySet());
96+
// Single param without @Param
97+
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
98+
}
99+
}
100+
101+
private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
102+
String[] keyProperties, Object parameter) throws SQLException {
103+
Collection<?> params = collectionize(parameter);
104+
if (params.isEmpty()) {
105+
return;
101106
}
102-
// Remove param name from 'keyProperty' string. e.g. 'param.id' -> 'id'
103-
String[] modifiedKeyProperties = new String[keyProperties.length];
107+
List<KeyAssigner> assignerList = new ArrayList<>();
104108
for (int i = 0; i < keyProperties.length; i++) {
105-
if (keyProperties[i].charAt(firstDot) == '.' && keyProperties[i].startsWith(paramName)) {
106-
modifiedKeyProperties[i] = keyProperties[i].substring(firstDot + 1);
107-
} else {
108-
throw new ExecutorException("Assigning generated keys to multiple parameters is not supported. "
109-
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
110-
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
111-
+ paramMap.keySet());
112-
}
109+
assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
110+
}
111+
Iterator<?> iterator = params.iterator();
112+
while (rs.next()) {
113+
Object param = iterator.next();
114+
assignerList.forEach(x -> x.assign(rs, param));
113115
}
114-
assignKeysToParam(configuration, rs, modifiedKeyProperties, param);
115116
}
116117

117-
private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties,
118-
Object param)
119-
throws SQLException {
120-
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
121-
final ResultSetMetaData rsmd = rs.getMetaData();
122-
// Wrap the parameter in Collection to normalize the logic.
123-
Collection<?> paramAsCollection;
124-
if (param instanceof Object[]) {
125-
paramAsCollection = Arrays.asList((Object[]) param);
126-
} else if (!(param instanceof Collection)) {
127-
paramAsCollection = Arrays.asList(param);
128-
} else {
129-
paramAsCollection = (Collection<?>) param;
130-
}
131-
TypeHandler<?>[] typeHandlers = null;
132-
for (Object obj : paramAsCollection) {
133-
if (!rs.next()) {
134-
break;
135-
}
136-
MetaObject metaParam = configuration.newMetaObject(obj);
137-
if (typeHandlers == null) {
138-
typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
118+
private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
119+
String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
120+
Iterator<ParamMap<?>> iterator = paramMapList.iterator();
121+
List<KeyAssigner> assignerList = new ArrayList<>();
122+
while (rs.next()) {
123+
ParamMap<?> paramMap = iterator.next();
124+
if (assignerList.isEmpty()) {
125+
for (int i = 0; i < keyProperties.length; i++) {
126+
assignerList
127+
.add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
128+
.getValue());
129+
}
139130
}
140-
populateKeys(rs, metaParam, keyProperties, typeHandlers);
131+
assignerList.forEach(x -> x.assign(rs, paramMap));
141132
}
142133
}
143134

144-
private Object getSoleParameter(Object parameter) {
145-
if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) {
146-
return parameter;
135+
private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
136+
String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
137+
if (paramMap.isEmpty()) {
138+
return;
139+
}
140+
Map<String, Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
141+
for (int i = 0; i < keyProperties.length; i++) {
142+
Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i],
143+
keyProperties, true);
144+
Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = assignerMap.computeIfAbsent(entry.getKey(),
145+
k -> entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
146+
iteratorPair.getValue().add(entry.getValue());
147147
}
148-
Object soleParam = null;
149-
for (Object paramValue : ((Map<?, ?>) parameter).values()) {
150-
if (soleParam == null) {
151-
soleParam = paramValue;
152-
} else if (soleParam != paramValue) {
153-
soleParam = null;
154-
break;
148+
while (rs.next()) {
149+
for (Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
150+
Object param = pair.getKey().next();
151+
pair.getValue().forEach(x -> x.assign(rs, param));
155152
}
156153
}
157-
return soleParam;
158154
}
159155

160-
private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
161-
TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
162-
for (int i = 0; i < keyProperties.length; i++) {
163-
if (metaParam.hasSetter(keyProperties[i])) {
164-
Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
165-
typeHandlers[i] = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
166-
} else {
167-
throw new ExecutorException("No setter found for the keyProperty '" + keyProperties[i] + "' in '"
168-
+ metaParam.getOriginalObject().getClass().getName() + "'.");
156+
private Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd,
157+
int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
158+
boolean singleParam = paramMap.values().stream().distinct().count() == 1;
159+
int firstDot = keyProperty.indexOf('.');
160+
if (firstDot == -1) {
161+
if (singleParam) {
162+
// Assume 'keyProperty' to be a property of the single param.
163+
String singleParamName = nameOfSingleParam(paramMap);
164+
String argParamName = omitParamName ? null : singleParamName;
165+
return entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
169166
}
167+
throw new ExecutorException("Could not determine which parameter to assign generated keys to. "
168+
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
169+
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
170+
+ paramMap.keySet());
171+
}
172+
String paramName = keyProperty.substring(0, firstDot);
173+
if (paramMap.containsKey(paramName)) {
174+
String argParamName = omitParamName ? null : paramName;
175+
String argKeyProperty = keyProperty.substring(firstDot + 1);
176+
return entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
177+
} else if (singleParam) {
178+
// Assume 'keyProperty' to be a property of the single param.
179+
String singleParamName = nameOfSingleParam(paramMap);
180+
String argParamName = omitParamName ? null : singleParamName;
181+
return entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
182+
} else {
183+
throw new ExecutorException("Could not find parameter '" + paramName + "'. "
184+
+ "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
185+
+ "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
186+
+ paramMap.keySet());
170187
}
171-
return typeHandlers;
172188
}
173189

174-
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
175-
for (int i = 0; i < keyProperties.length; i++) {
176-
String property = keyProperties[i];
177-
TypeHandler<?> th = typeHandlers[i];
178-
if (th != null) {
179-
Object value = th.getResult(rs, i + 1);
180-
metaParam.setValue(property, value);
181-
}
190+
private static String nameOfSingleParam(Map<String, ?> paramMap) {
191+
// There is virtually one parameter, so any key works.
192+
return paramMap.keySet().iterator().next();
193+
}
194+
195+
private static Collection<?> collectionize(Object param) {
196+
if (param instanceof Collection) {
197+
return (Collection<?>) param;
198+
} else if (param instanceof Object[]) {
199+
return Arrays.asList((Object[]) param);
200+
} else {
201+
return Arrays.asList(param);
182202
}
183203
}
184204

205+
private static <K, V> Entry<K, V> entry(K key, V value) {
206+
// Replace this with Map.entry(key, value) in Java 9.
207+
return new AbstractMap.SimpleImmutableEntry<>(key, value);
208+
}
209+
210+
private class KeyAssigner {
211+
protected final Configuration configuration;
212+
protected final ResultSetMetaData rsmd;
213+
protected final TypeHandlerRegistry typeHandlerRegistry;
214+
protected final int columnPosition;
215+
protected final String paramName;
216+
protected final String propertyName;
217+
protected TypeHandler<?> typeHandler;
218+
219+
protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
220+
String propertyName) {
221+
super();
222+
this.configuration = configuration;
223+
this.rsmd = rsmd;
224+
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
225+
this.columnPosition = columnPosition;
226+
this.paramName = paramName;
227+
this.propertyName = propertyName;
228+
}
229+
230+
protected void assign(ResultSet rs, Object param) {
231+
if (paramName != null) {
232+
// If paramName is set, param is ParamMap
233+
param = ((ParamMap<?>) param).get(paramName);
234+
}
235+
MetaObject metaParam = configuration.newMetaObject(param);
236+
try {
237+
if (typeHandler == null) {
238+
if (metaParam.hasSetter(propertyName)) {
239+
Class<?> propertyType = metaParam.getSetterType(propertyName);
240+
typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
241+
JdbcType.forCode(rsmd.getColumnType(columnPosition)));
242+
} else {
243+
throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
244+
+ metaParam.getOriginalObject().getClass().getName() + "'.");
245+
}
246+
}
247+
if (typeHandler == null) {
248+
// Error?
249+
} else {
250+
Object value = typeHandler.getResult(rs, columnPosition);
251+
metaParam.setValue(propertyName, value);
252+
}
253+
} catch (SQLException e) {
254+
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
255+
e);
256+
}
257+
}
258+
}
185259
}

src/test/java/org/apache/ibatis/submitted/keygen/CountryMapper.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2018 the original author or authors.
2+
* Copyright 2009-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,6 +33,10 @@ public interface CountryMapper {
3333
@Insert({ "insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})" })
3434
int insertNamedBean(@Param("country") Country country);
3535

36+
@Options(useGeneratedKeys = true, keyProperty = "country.id")
37+
@Insert({ "insert into country (countryname,countrycode) values (#{country.countryname},#{country.countrycode})" })
38+
int insertNamedBean_keyPropertyWithParamName(@Param("country") Country country);
39+
3640
int insertList(List<Country> countries);
3741

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

0 commit comments

Comments
 (0)