Skip to content

Commit 2a898b3

Browse files
authored
Merge pull request #349 from jeffgbutler/better-insert-multiple-support
Add Better Support for MyBatis Multi-Row Inserts that Return Generated Keys
2 parents b67ea69 + 38db33d commit 2a898b3

File tree

12 files changed

+323
-38
lines changed

12 files changed

+323
-38
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Kotlin DSL.
9999
- Added composition functions for WhereApplier ([#335](https://github.com/mybatis/mybatis-dynamic-sql/pull/335))
100100
- Added a mapping for general insert and update statements that will render null values as "null" in the SQL ([#343](https://github.com/mybatis/mybatis-dynamic-sql/pull/343))
101101
- Allow the "in when present" conditions to accept a null Collection as a parameter ([#346](https://github.com/mybatis/mybatis-dynamic-sql/pull/346))
102+
- Add Better Support for MyBatis Multi-Row Inserts that Return Generated Keys ([#349](https://github.com/mybatis/mybatis-dynamic-sql/pull/349))
102103

103104
## Release 1.2.1 - September 29, 2020
104105

src/main/java/org/mybatis/dynamic/sql/util/SqlProviderAdapter.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 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.
@@ -23,6 +23,10 @@
2323
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
2424
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
2525

26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.stream.Collectors;
29+
2630
/**
2731
* Adapter for use with MyBatis SQL provider annotations.
2832
*
@@ -47,6 +51,30 @@ public String insertMultiple(MultiRowInsertStatementProvider<?> insertStatement)
4751
return insertStatement.getInsertStatement();
4852
}
4953

54+
/**
55+
* This adapter method is intended for use with MyBatis' @InsertProvider annotation when there are generated
56+
* values expected from executing the insert statement.
57+
*
58+
* @param parameterMap The parameter map is automatically created by MyBatis when there are multiple
59+
* parameters in the insert method.
60+
* @return the SQL statement contained in the parameter map. This is assumed to be the one
61+
* and only map entry of type String.
62+
*/
63+
public String insertMultipleWithGeneratedKeys(Map<String, Object> parameterMap) {
64+
List<String> entries = parameterMap.entrySet().stream()
65+
.filter(e -> e.getKey().startsWith("param")) //$NON-NLS-1$
66+
.filter(e -> String.class.isAssignableFrom(e.getValue().getClass()))
67+
.map(e -> (String) e.getValue())
68+
.collect(Collectors.toList());
69+
70+
if (entries.size() == 1) {
71+
return entries.get(0);
72+
} else {
73+
throw new IllegalArgumentException("The parameters for insertMultipleWithGeneratedKeys" + //$NON-NLS-1$
74+
" must contain exactly one parameter of type String"); //$NON-NLS-1$
75+
}
76+
}
77+
5078
public String insertSelect(InsertSelectStatementProvider insertStatement) {
5179
return insertStatement.getInsertStatement();
5280
}

src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 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.
@@ -18,6 +18,7 @@
1818
import java.util.Collection;
1919
import java.util.List;
2020
import java.util.function.Function;
21+
import java.util.function.ToIntBiFunction;
2122
import java.util.function.ToIntFunction;
2223
import java.util.function.ToLongFunction;
2324
import java.util.function.UnaryOperator;
@@ -138,6 +139,12 @@ public static <R> int insertMultiple(ToIntFunction<MultiRowInsertStatementProvid
138139
return mapper.applyAsInt(insertMultiple(records, table, completer));
139140
}
140141

142+
public static <R> int insertMultipleWithGeneratedKeys(ToIntBiFunction<String, List<R>> mapper,
143+
Collection<R> records, SqlTable table, UnaryOperator<MultiRowInsertDSL<R>> completer) {
144+
MultiRowInsertStatementProvider<R> provider = insertMultiple(records, table, completer);
145+
return mapper.applyAsInt(provider.getInsertStatement(), provider.getRecords());
146+
}
147+
141148
public static SelectStatementProvider select(BasicColumn[] selectList, SqlTable table,
142149
SelectDSLCompleter completer) {
143150
return select(SqlBuilder.select(selectList).from(table), completer);

src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/MapperSupportFunctions.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 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.
@@ -67,6 +67,16 @@ fun <T> insertMultiple(
6767
) =
6868
mapper(SqlBuilder.insertMultiple(records).into(table, completer))
6969

70+
fun <T> insertMultipleWithGeneratedKeys(
71+
mapper: (String, List<T>) -> Int,
72+
records: Collection<T>,
73+
table: SqlTable,
74+
completer: MultiRowInsertCompleter<T>
75+
): Int =
76+
with(SqlBuilder.insertMultiple(records).into(table, completer)) {
77+
mapper(insertStatement, this.records)
78+
}
79+
7080
fun insertSelect(
7181
mapper: (InsertSelectStatementProvider) -> Int,
7282
table: SqlTable,

src/site/markdown/docs/insert.md

+16-7
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,34 @@ The XML element should look like this:
161161
```
162162

163163
### Generated Values
164-
MyBatis supports returning generated values from a multiple row insert statement with some limitations. The main limitation is that MyBatis does not support nested lists in parameter objects. Unfortunately, the `MultiRowInsertStatementProvider` relies on a nested List. It is likely this limitation in MyBatis will be removed at some point in the future, so stay tuned.
164+
MyBatis supports returning generated values from a multiple row insert statement with some limitations. The main
165+
limitation is that MyBatis does not support nested lists in parameter objects. Unfortunately, the
166+
`MultiRowInsertStatementProvider` relies on a nested List. It is likely this limitation in MyBatis will be removed at
167+
some point in the future, so stay tuned.
165168

166-
Nevertheless, you can configure a mapper that will work with the `MultiRowInsertStatementProvider` as created by this library. The main idea is to decompose the statement from the parameter map and send them as separate parameters to the MyBatis mapper. For example:
169+
Nevertheless, you can configure a mapper that will work with the `MultiRowInsertStatementProvider` as created by this
170+
library. The main idea is to decompose the statement from the parameter map and send them as separate parameters to the
171+
MyBatis mapper. For example:
167172

168173
```java
169174
...
170-
@Insert({
171-
"${insertStatement}"
172-
})
175+
@InsertProvider(type=SqlProviderAdapter.class, method="insertMultipleWithGeneratedKeys")
173176
@Options(useGeneratedKeys=true, keyProperty="records.fullName")
174-
int insertMultipleWithGeneratedKeys(@Param("insertStatement") String statement, @Param("records") List<GeneratedAlwaysRecord> records);
177+
int insertMultipleWithGeneratedKeys(String insertStatement, @Param("records") List<GeneratedAlwaysRecord> records);
175178

176179
default int insertMultipleWithGeneratedKeys(MultiRowInsertStatementProvider<GeneratedAlwaysRecord> multiInsert) {
177180
return insertMultipleWithGeneratedKeys(multiInsert.getInsertStatement(), multiInsert.getRecords());
178181
}
179182
...
180183
```
181184

182-
The first method above shows the actual MyBatis mapper method. Note the use of the `@Options` annotation to specify that we expect generated values. Further note that the `keyProperty` is set to `records.fullName` - in this case, `fullName` is a property of the objects in the `records` List.
185+
The first method above shows the actual MyBatis mapper method. Note the use of the `@Options` annotation to specify
186+
that we expect generated values. Further, note that the `keyProperty` is set to `records.fullName` - in this case,
187+
`fullName` is a property of the objects in the `records` List. The library supplied adapter method will simply
188+
return the `insertStatement` as supplied in the method call. The adapter method requires that there be one, and only
189+
one, String parameter in the method call, and it assumes that this one String parameter is the SQL insert statement.
190+
The parameter can have any name and can be specified in any position in the method's parameter list.
191+
The `@Param` annotation is not required for the insert statement. However, it may be specified if you so desire.
183192

184193
The second method above decomposes the `MultiRowInsertStatementProvider` and calls the first method.
185194

src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapper.java

+15-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 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.
@@ -23,7 +23,6 @@
2323
import java.util.List;
2424
import java.util.Optional;
2525

26-
import org.apache.ibatis.annotations.Insert;
2726
import org.apache.ibatis.annotations.InsertProvider;
2827
import org.apache.ibatis.annotations.Options;
2928
import org.apache.ibatis.annotations.Param;
@@ -33,7 +32,6 @@
3332
import org.apache.ibatis.annotations.SelectProvider;
3433
import org.mybatis.dynamic.sql.BasicColumn;
3534
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider;
36-
import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider;
3735
import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
3836
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
3937
import org.mybatis.dynamic.sql.update.UpdateDSL;
@@ -42,11 +40,10 @@
4240
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
4341

4442
import examples.generated.always.GeneratedAlwaysRecord;
45-
import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper;
4643
import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper;
4744
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
4845

49-
public interface GeneratedAlwaysMapper extends CommonInsertMapper<GeneratedAlwaysRecord>, CommonUpdateMapper {
46+
public interface GeneratedAlwaysMapper extends CommonUpdateMapper {
5047
@SelectProvider(type=SqlProviderAdapter.class, method="select")
5148
@Results(id="gaResults", value={
5249
@Result(property="id", column="id", id=true),
@@ -60,23 +57,14 @@ public interface GeneratedAlwaysMapper extends CommonInsertMapper<GeneratedAlway
6057
@ResultMap("gaResults")
6158
Optional<GeneratedAlwaysRecord> selectOne(SelectStatementProvider selectStatement);
6259

63-
@Override
6460
@InsertProvider(type=SqlProviderAdapter.class, method="insert")
6561
@Options(useGeneratedKeys=true, keyProperty="record.fullName")
6662
int insert(InsertStatementProvider<GeneratedAlwaysRecord> insertStatement);
6763

68-
// This is kludgy. Currently MyBatis does not support nested lists in parameter objects
69-
// when returning generated keys.
70-
// So we need to do this silliness and decompose the multi row insert into its component parts
71-
// for the actual MyBatis call
72-
@Insert("${insertStatement}")
64+
@InsertProvider(type=SqlProviderAdapter.class, method="insertMultipleWithGeneratedKeys")
7365
@Options(useGeneratedKeys=true, keyProperty="records.fullName")
7466
int insertMultiple(@Param("insertStatement") String statement, @Param("records") List<GeneratedAlwaysRecord> records);
7567

76-
default int insertMultiple(MultiRowInsertStatementProvider<GeneratedAlwaysRecord> multiInsert) {
77-
return insertMultiple(multiInsert.getInsertStatement(), multiInsert.getRecords());
78-
}
79-
8068
BasicColumn[] selectList =
8169
BasicColumn.columnList(id, firstName, lastName, fullName);
8270

@@ -100,6 +88,18 @@ default int insert(GeneratedAlwaysRecord record) {
10088
);
10189
}
10290

91+
default int insertMultiple(GeneratedAlwaysRecord...records) {
92+
return insertMultiple(Arrays.asList(records));
93+
}
94+
95+
default int insertMultiple(Collection<GeneratedAlwaysRecord> records) {
96+
return MyBatis3Utils.insertMultipleWithGeneratedKeys(this::insertMultiple, records, generatedAlways, c ->
97+
c.map(id).toProperty("id")
98+
.map(firstName).toProperty("firstName")
99+
.map(lastName).toProperty("lastName")
100+
);
101+
}
102+
103103
default int insertSelective(GeneratedAlwaysRecord record) {
104104
return MyBatis3Utils.insert(this::insert, record, generatedAlways, c ->
105105
c.map(id).toPropertyWhenPresent("id", record::getId)
@@ -133,16 +133,4 @@ static UpdateDSL<UpdateModel> updateSelectiveColumns(GeneratedAlwaysRecord recor
133133
.set(firstName).equalToWhenPresent(record::getFirstName)
134134
.set(lastName).equalToWhenPresent(record::getLastName);
135135
}
136-
137-
default int insertMultiple(GeneratedAlwaysRecord...records) {
138-
return insertMultiple(Arrays.asList(records));
139-
}
140-
141-
default int insertMultiple(Collection<GeneratedAlwaysRecord> records) {
142-
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, generatedAlways, c ->
143-
c.map(id).toProperty("id")
144-
.map(firstName).toProperty("firstName")
145-
.map(lastName).toProperty("lastName")
146-
);
147-
}
148136
}

src/test/java/examples/generated/always/mybatis/GeneratedAlwaysMapperTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ void testMultiInsertWithArrayAndVariousMappings() {
227227

228228
assertThat(multiRowInsert.getInsertStatement()).isEqualTo(statement);
229229

230-
int rows = mapper.insertMultiple(multiRowInsert);
230+
int rows = mapper.insertMultiple(multiRowInsert.getInsertStatement(), multiRowInsert.getRecords());
231231

232232
assertAll(
233233
() -> assertThat(rows).isEqualTo(1),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2016-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql.util;
17+
18+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
25+
class SqlProviderAdapterTest {
26+
@Test
27+
void testThatInsertMultipleWithGeneratedKeysThrowsException() {
28+
Map<String, Object> parameters = new HashMap<>();
29+
SqlProviderAdapter adapter = new SqlProviderAdapter();
30+
31+
assertThatExceptionOfType(IllegalArgumentException.class)
32+
.isThrownBy(() -> adapter.insertMultipleWithGeneratedKeys(parameters))
33+
.withMessage("The parameters for insertMultipleWithGeneratedKeys must contain exactly one parameter of type String");
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2016-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package examples.kotlin.mybatis3.canonical
17+
18+
import org.mybatis.dynamic.sql.SqlTable
19+
import java.sql.JDBCType
20+
21+
object GeneratedAlwaysDynamicSqlSupport {
22+
val generatedAlways = GeneratedAlways()
23+
val id = generatedAlways.id
24+
val firstName = generatedAlways.firstName
25+
val lastName = generatedAlways.lastName
26+
val fullName = generatedAlways.fullName
27+
28+
class GeneratedAlways : SqlTable("GeneratedAlways") {
29+
val id = column<Int>("id", JDBCType.INTEGER)
30+
val firstName = column<String>("first_name", JDBCType.VARCHAR)
31+
val lastName = column<String>("last_name", JDBCType.VARCHAR)
32+
val fullName = column<String>("full_name", JDBCType.VARCHAR)
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2016-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package examples.kotlin.mybatis3.canonical
17+
18+
import examples.kotlin.mybatis3.canonical.GeneratedAlwaysDynamicSqlSupport.firstName
19+
import examples.kotlin.mybatis3.canonical.GeneratedAlwaysDynamicSqlSupport.generatedAlways
20+
import examples.kotlin.mybatis3.canonical.GeneratedAlwaysDynamicSqlSupport.lastName
21+
import org.apache.ibatis.annotations.InsertProvider
22+
import org.apache.ibatis.annotations.Options
23+
import org.apache.ibatis.annotations.Param
24+
import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider
25+
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider
26+
import org.mybatis.dynamic.sql.util.SqlProviderAdapter
27+
import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insert
28+
import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertMultipleWithGeneratedKeys
29+
30+
interface GeneratedAlwaysMapper {
31+
@InsertProvider(type = SqlProviderAdapter::class, method = "insert")
32+
@Options(useGeneratedKeys = true, keyProperty="record.id,record.fullName", keyColumn = "id,full_name")
33+
fun insert(insertStatement: InsertStatementProvider<GeneratedAlwaysRecord>): Int
34+
35+
@InsertProvider(type = SqlProviderAdapter::class, method = "generalInsert")
36+
fun generalInsert(insertStatement: GeneralInsertStatementProvider): Int
37+
38+
@InsertProvider(type = SqlProviderAdapter::class, method = "insertMultipleWithGeneratedKeys")
39+
@Options(useGeneratedKeys = true, keyProperty="records.id,records.fullName", keyColumn = "id,full_name")
40+
fun insertMultiple(insertStatement: String, @Param("records") records: List<GeneratedAlwaysRecord>): Int
41+
}
42+
43+
fun GeneratedAlwaysMapper.insert(record: GeneratedAlwaysRecord): Int {
44+
return insert(this::insert, record, generatedAlways) {
45+
map(firstName).toProperty("firstName")
46+
map(lastName).toProperty("lastName")
47+
}
48+
}
49+
50+
fun GeneratedAlwaysMapper.insertMultiple(records: Collection<GeneratedAlwaysRecord>): Int {
51+
return insertMultipleWithGeneratedKeys(this::insertMultiple, records, generatedAlways) {
52+
map(firstName).toProperty("firstName")
53+
map(lastName).toProperty("lastName")
54+
}
55+
}

0 commit comments

Comments
 (0)