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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 29 additions & 1 deletion
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 16 additions & 7 deletions
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

Lines changed: 15 additions & 27 deletions
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

Lines changed: 1 addition & 1 deletion
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),
Lines changed: 35 additions & 0 deletions
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+
}
Lines changed: 34 additions & 0 deletions
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+
}
Lines changed: 55 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy