Skip to content

Add Better Support for MyBatis Multi-Row Inserts that Return Generated Keys #349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Kotlin DSL.
- Added composition functions for WhereApplier ([#335](https://github.com/mybatis/mybatis-dynamic-sql/pull/335))
- 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))
- Allow the "in when present" conditions to accept a null Collection as a parameter ([#346](https://github.com/mybatis/mybatis-dynamic-sql/pull/346))
- Add Better Support for MyBatis Multi-Row Inserts that Return Generated Keys ([#349](https://github.com/mybatis/mybatis-dynamic-sql/pull/349))

## Release 1.2.1 - September 29, 2020

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2021 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 @@ -23,6 +23,10 @@
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Adapter for use with MyBatis SQL provider annotations.
*
Expand All @@ -47,6 +51,30 @@ public String insertMultiple(MultiRowInsertStatementProvider<?> insertStatement)
return insertStatement.getInsertStatement();
}

/**
* This adapter method is intended for use with MyBatis' @InsertProvider annotation when there are generated
* values expected from executing the insert statement.
*
* @param parameterMap The parameter map is automatically created by MyBatis when there are multiple
* parameters in the insert method.
* @return the SQL statement contained in the parameter map. This is assumed to be the one
* and only map entry of type String.
*/
public String insertMultipleWithGeneratedKeys(Map<String, Object> parameterMap) {
List<String> entries = parameterMap.entrySet().stream()
.filter(e -> e.getKey().startsWith("param")) //$NON-NLS-1$
.filter(e -> String.class.isAssignableFrom(e.getValue().getClass()))
.map(e -> (String) e.getValue())
.collect(Collectors.toList());

if (entries.size() == 1) {
return entries.get(0);
} else {
throw new IllegalArgumentException("The parameters for insertMultipleWithGeneratedKeys" + //$NON-NLS-1$
" must contain exactly one parameter of type String"); //$NON-NLS-1$
}
}

public String insertSelect(InsertSelectStatementProvider insertStatement) {
return insertStatement.getInsertStatement();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2021 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 @@ -18,6 +18,7 @@
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.ToIntBiFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.function.UnaryOperator;
Expand Down Expand Up @@ -138,6 +139,12 @@ public static <R> int insertMultiple(ToIntFunction<MultiRowInsertStatementProvid
return mapper.applyAsInt(insertMultiple(records, table, completer));
}

public static <R> int insertMultipleWithGeneratedKeys(ToIntBiFunction<String, List<R>> mapper,
Collection<R> records, SqlTable table, UnaryOperator<MultiRowInsertDSL<R>> completer) {
MultiRowInsertStatementProvider<R> provider = insertMultiple(records, table, completer);
return mapper.applyAsInt(provider.getInsertStatement(), provider.getRecords());
}

public static SelectStatementProvider select(BasicColumn[] selectList, SqlTable table,
SelectDSLCompleter completer) {
return select(SqlBuilder.select(selectList).from(table), completer);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2021 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 Down Expand Up @@ -67,6 +67,16 @@ fun <T> insertMultiple(
) =
mapper(SqlBuilder.insertMultiple(records).into(table, completer))

fun <T> insertMultipleWithGeneratedKeys(
mapper: (String, List<T>) -> Int,
records: Collection<T>,
table: SqlTable,
completer: MultiRowInsertCompleter<T>
): Int =
with(SqlBuilder.insertMultiple(records).into(table, completer)) {
mapper(insertStatement, this.records)
}

fun insertSelect(
mapper: (InsertSelectStatementProvider) -> Int,
table: SqlTable,
Expand Down
23 changes: 16 additions & 7 deletions src/site/markdown/docs/insert.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,25 +161,34 @@ The XML element should look like this:
```

### Generated Values
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.
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.

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:
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:

```java
...
@Insert({
"${insertStatement}"
})
@InsertProvider(type=SqlProviderAdapter.class, method="insertMultipleWithGeneratedKeys")
@Options(useGeneratedKeys=true, keyProperty="records.fullName")
int insertMultipleWithGeneratedKeys(@Param("insertStatement") String statement, @Param("records") List<GeneratedAlwaysRecord> records);
int insertMultipleWithGeneratedKeys(String insertStatement, @Param("records") List<GeneratedAlwaysRecord> records);

default int insertMultipleWithGeneratedKeys(MultiRowInsertStatementProvider<GeneratedAlwaysRecord> multiInsert) {
return insertMultipleWithGeneratedKeys(multiInsert.getInsertStatement(), multiInsert.getRecords());
}
...
```

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.
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. The library supplied adapter method will simply
return the `insertStatement` as supplied in the method call. The adapter method requires that there be one, and only
one, String parameter in the method call, and it assumes that this one String parameter is the SQL insert statement.
The parameter can have any name and can be specified in any position in the method's parameter list.
The `@Param` annotation is not required for the insert statement. However, it may be specified if you so desire.

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2021 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 @@ -23,7 +23,6 @@
import java.util.List;
import java.util.Optional;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
Expand All @@ -33,7 +32,6 @@
import org.apache.ibatis.annotations.SelectProvider;
import org.mybatis.dynamic.sql.BasicColumn;
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider;
import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider;
import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.update.UpdateDSL;
Expand All @@ -42,11 +40,10 @@
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;

import examples.generated.always.GeneratedAlwaysRecord;
import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper;
import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper;
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;

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

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

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

default int insertMultiple(MultiRowInsertStatementProvider<GeneratedAlwaysRecord> multiInsert) {
return insertMultiple(multiInsert.getInsertStatement(), multiInsert.getRecords());
}

BasicColumn[] selectList =
BasicColumn.columnList(id, firstName, lastName, fullName);

Expand All @@ -100,6 +88,18 @@ default int insert(GeneratedAlwaysRecord record) {
);
}

default int insertMultiple(GeneratedAlwaysRecord...records) {
return insertMultiple(Arrays.asList(records));
}

default int insertMultiple(Collection<GeneratedAlwaysRecord> records) {
return MyBatis3Utils.insertMultipleWithGeneratedKeys(this::insertMultiple, records, generatedAlways, c ->
c.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
);
}

default int insertSelective(GeneratedAlwaysRecord record) {
return MyBatis3Utils.insert(this::insert, record, generatedAlways, c ->
c.map(id).toPropertyWhenPresent("id", record::getId)
Expand Down Expand Up @@ -133,16 +133,4 @@ static UpdateDSL<UpdateModel> updateSelectiveColumns(GeneratedAlwaysRecord recor
.set(firstName).equalToWhenPresent(record::getFirstName)
.set(lastName).equalToWhenPresent(record::getLastName);
}

default int insertMultiple(GeneratedAlwaysRecord...records) {
return insertMultiple(Arrays.asList(records));
}

default int insertMultiple(Collection<GeneratedAlwaysRecord> records) {
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, generatedAlways, c ->
c.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ void testMultiInsertWithArrayAndVariousMappings() {

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

int rows = mapper.insertMultiple(multiRowInsert);
int rows = mapper.insertMultiple(multiRowInsert.getInsertStatement(), multiRowInsert.getRecords());

assertAll(
() -> assertThat(rows).isEqualTo(1),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2016-2021 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.dynamic.sql.util;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

class SqlProviderAdapterTest {
@Test
void testThatInsertMultipleWithGeneratedKeysThrowsException() {
Map<String, Object> parameters = new HashMap<>();
SqlProviderAdapter adapter = new SqlProviderAdapter();

assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> adapter.insertMultipleWithGeneratedKeys(parameters))
.withMessage("The parameters for insertMultipleWithGeneratedKeys must contain exactly one parameter of type String");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2016-2021 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package examples.kotlin.mybatis3.canonical

import org.mybatis.dynamic.sql.SqlTable
import java.sql.JDBCType

object GeneratedAlwaysDynamicSqlSupport {
val generatedAlways = GeneratedAlways()
val id = generatedAlways.id
val firstName = generatedAlways.firstName
val lastName = generatedAlways.lastName
val fullName = generatedAlways.fullName

class GeneratedAlways : SqlTable("GeneratedAlways") {
val id = column<Int>("id", JDBCType.INTEGER)
val firstName = column<String>("first_name", JDBCType.VARCHAR)
val lastName = column<String>("last_name", JDBCType.VARCHAR)
val fullName = column<String>("full_name", JDBCType.VARCHAR)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2016-2021 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package examples.kotlin.mybatis3.canonical

import examples.kotlin.mybatis3.canonical.GeneratedAlwaysDynamicSqlSupport.firstName
import examples.kotlin.mybatis3.canonical.GeneratedAlwaysDynamicSqlSupport.generatedAlways
import examples.kotlin.mybatis3.canonical.GeneratedAlwaysDynamicSqlSupport.lastName
import org.apache.ibatis.annotations.InsertProvider
import org.apache.ibatis.annotations.Options
import org.apache.ibatis.annotations.Param
import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider
import org.mybatis.dynamic.sql.util.SqlProviderAdapter
import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insert
import org.mybatis.dynamic.sql.util.kotlin.mybatis3.insertMultipleWithGeneratedKeys

interface GeneratedAlwaysMapper {
@InsertProvider(type = SqlProviderAdapter::class, method = "insert")
@Options(useGeneratedKeys = true, keyProperty="record.id,record.fullName", keyColumn = "id,full_name")
fun insert(insertStatement: InsertStatementProvider<GeneratedAlwaysRecord>): Int

@InsertProvider(type = SqlProviderAdapter::class, method = "generalInsert")
fun generalInsert(insertStatement: GeneralInsertStatementProvider): Int

@InsertProvider(type = SqlProviderAdapter::class, method = "insertMultipleWithGeneratedKeys")
@Options(useGeneratedKeys = true, keyProperty="records.id,records.fullName", keyColumn = "id,full_name")
fun insertMultiple(insertStatement: String, @Param("records") records: List<GeneratedAlwaysRecord>): Int
}

fun GeneratedAlwaysMapper.insert(record: GeneratedAlwaysRecord): Int {
return insert(this::insert, record, generatedAlways) {
map(firstName).toProperty("firstName")
map(lastName).toProperty("lastName")
}
}

fun GeneratedAlwaysMapper.insertMultiple(records: Collection<GeneratedAlwaysRecord>): Int {
return insertMultipleWithGeneratedKeys(this::insertMultiple, records, generatedAlways) {
map(firstName).toProperty("firstName")
map(lastName).toProperty("lastName")
}
}
Loading