Skip to content

Commit 582ac9e

Browse files
committed
Add Dialect IdGeneration property to indicate support for batch operations.
+ SqlServer and DB2 Dialects do not support id generation for batch operations. + BatchInsertStrategy delegates to InsertStrategy when id generation not supported for batch operations.
1 parent 01b4469 commit 582ac9e

File tree

13 files changed

+570
-333
lines changed

13 files changed

+570
-333
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.springframework.data.jdbc.core.convert;
2+
3+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
4+
5+
interface BatchInsertStrategy {
6+
Object[] execute(String sql, SqlParameterSource[] sqlParameterSources);
7+
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public <T> Object[] insert(List<RecordDescriptor<T>> recordDescriptors, Class<T>
121121

122122
String insertSql = sql(domainType).getInsert(sqlParameterSources[0].getIdentifiers());
123123

124-
return insertStrategyFactory.insertStrategy(!includeId, getIdColumn(domainType)).execute(insertSql, sqlParameterSources);
124+
return insertStrategyFactory.batchInsertStrategy(!includeId, getIdColumn(domainType)).execute(insertSql, sqlParameterSources);
125125
}
126126

127127
/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.springframework.data.jdbc.core.convert;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Optional;
7+
8+
import org.springframework.data.relational.core.dialect.Dialect;
9+
import org.springframework.data.relational.core.dialect.IdGeneration;
10+
import org.springframework.data.relational.core.sql.SqlIdentifier;
11+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
12+
import org.springframework.jdbc.support.GeneratedKeyHolder;
13+
import org.springframework.lang.Nullable;
14+
15+
class IdGeneratingBatchInsertStrategy implements BatchInsertStrategy {
16+
17+
private final InsertStrategy insertStrategy;
18+
private final Dialect dialect;
19+
private final BatchJdbcOperations batchJdbcOperations;
20+
private final SqlIdentifier idColumn;
21+
22+
public IdGeneratingBatchInsertStrategy(InsertStrategy insertStrategy,
23+
Dialect dialect, BatchJdbcOperations batchJdbcOperations,
24+
@Nullable SqlIdentifier idColumn) {
25+
this.insertStrategy = insertStrategy;
26+
this.dialect = dialect;
27+
this.batchJdbcOperations = batchJdbcOperations;
28+
this.idColumn = idColumn;
29+
}
30+
31+
@Override
32+
public Object[] execute(String sql, SqlParameterSource[] sqlParameterSources) {
33+
34+
if (!dialect.getIdGeneration().supportedForBatchOperations()) {
35+
return Arrays.stream(sqlParameterSources)
36+
.map(sqlParameterSource -> insertStrategy.execute(sql, sqlParameterSource))
37+
.toArray();
38+
}
39+
40+
GeneratedKeyHolder holder = new GeneratedKeyHolder();
41+
IdGeneration idGeneration = dialect.getIdGeneration();
42+
if (idGeneration.driverRequiresKeyColumnNames()) {
43+
44+
String[] keyColumnNames = getKeyColumnNames();
45+
if (keyColumnNames.length == 0) {
46+
batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder);
47+
} else {
48+
batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder, keyColumnNames);
49+
}
50+
} else {
51+
batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder);
52+
}
53+
Object[] ids = new Object[sqlParameterSources.length];
54+
List<Map<String, Object>> keyList = holder.getKeyList();
55+
for (int i = 0; i < keyList.size(); i++) {
56+
Map<String, Object> keys = keyList.get(i);
57+
if (keys.size() > 1) {
58+
if (idColumn != null) {
59+
ids[i] = keys.get(idColumn.getReference(dialect.getIdentifierProcessing()));
60+
}
61+
} else {
62+
ids[i] = keys.entrySet().stream().findFirst() //
63+
.map(Map.Entry::getValue) //
64+
.orElseThrow(() -> new IllegalStateException("KeyHolder contains an empty key list."));
65+
}
66+
}
67+
return ids;
68+
}
69+
70+
private String[] getKeyColumnNames() {
71+
return Optional.ofNullable(idColumn)
72+
.map(idColumn -> new String[]{idColumn.getReference(dialect.getIdentifierProcessing())})
73+
.orElse(new String[0]);
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.springframework.data.jdbc.core.convert;
2+
3+
import org.springframework.dao.DataRetrievalFailureException;
4+
import org.springframework.dao.InvalidDataAccessApiUsageException;
5+
import org.springframework.data.relational.core.dialect.Dialect;
6+
import org.springframework.data.relational.core.dialect.IdGeneration;
7+
import org.springframework.data.relational.core.sql.SqlIdentifier;
8+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
9+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
10+
import org.springframework.jdbc.support.GeneratedKeyHolder;
11+
import org.springframework.jdbc.support.KeyHolder;
12+
import org.springframework.lang.Nullable;
13+
14+
import java.util.Map;
15+
import java.util.Optional;
16+
17+
class IdGeneratingInsertStrategy implements InsertStrategy {
18+
19+
private final Dialect dialect;
20+
private final NamedParameterJdbcOperations jdbcOperations;
21+
private final SqlIdentifier idColumn;
22+
23+
public IdGeneratingInsertStrategy(Dialect dialect, NamedParameterJdbcOperations jdbcOperations,
24+
@Nullable SqlIdentifier idColumn) {
25+
this.dialect = dialect;
26+
this.jdbcOperations = jdbcOperations;
27+
this.idColumn = idColumn;
28+
}
29+
30+
@Override
31+
public Object execute(String sql, SqlParameterSource sqlParameterSource) {
32+
33+
KeyHolder holder = new GeneratedKeyHolder();
34+
35+
IdGeneration idGeneration = dialect.getIdGeneration();
36+
37+
if (idGeneration.driverRequiresKeyColumnNames()) {
38+
39+
String[] keyColumnNames = getKeyColumnNames();
40+
if (keyColumnNames.length == 0) {
41+
jdbcOperations.update(sql, sqlParameterSource, holder);
42+
} else {
43+
jdbcOperations.update(sql, sqlParameterSource, holder, keyColumnNames);
44+
}
45+
} else {
46+
jdbcOperations.update(sql, sqlParameterSource, holder);
47+
}
48+
49+
try {
50+
// MySQL just returns one value with a special name
51+
return holder.getKey();
52+
} catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) {
53+
// Postgres returns a value for each column
54+
// MS SQL Server returns a value that might be null.
55+
56+
Map<String, Object> keys = holder.getKeys();
57+
if (keys == null || idColumn == null) {
58+
return null;
59+
}
60+
61+
return keys.get(idColumn.getReference(dialect.getIdentifierProcessing()));
62+
}
63+
}
64+
65+
private String[] getKeyColumnNames() {
66+
return Optional.ofNullable(idColumn)
67+
.map(idColumn -> new String[]{idColumn.getReference(dialect.getIdentifierProcessing())})
68+
.orElse(new String[0]);
69+
}
70+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.springframework.data.jdbc.core.convert;
2+
3+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
4+
import org.springframework.lang.Nullable;
5+
6+
interface InsertStrategy {
7+
@Nullable
8+
Object execute(String sql, SqlParameterSource sqlParameterSource);
9+
}
Lines changed: 16 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
package org.springframework.data.jdbc.core.convert;
22

3-
import java.util.List;
4-
import java.util.Map;
5-
import java.util.Optional;
6-
7-
import org.springframework.dao.DataRetrievalFailureException;
8-
import org.springframework.dao.InvalidDataAccessApiUsageException;
93
import org.springframework.data.relational.core.dialect.Dialect;
10-
import org.springframework.data.relational.core.dialect.IdGeneration;
114
import org.springframework.data.relational.core.sql.SqlIdentifier;
125
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
136
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
14-
import org.springframework.jdbc.support.GeneratedKeyHolder;
15-
import org.springframework.jdbc.support.KeyHolder;
167
import org.springframework.lang.Nullable;
178

189
public class InsertStrategyFactory {
@@ -29,19 +20,22 @@ public InsertStrategyFactory(NamedParameterJdbcOperations namedParameterJdbcOper
2920

3021
InsertStrategy insertStrategy(boolean generateIds, @Nullable SqlIdentifier idColumn) {
3122
if (generateIds) {
32-
return new IdGeneratingInsertStrategy(dialect, namedParameterJdbcOperations, batchJdbcOperations, idColumn);
23+
return new IdGeneratingInsertStrategy(dialect, namedParameterJdbcOperations, idColumn);
3324
} else {
3425
return new DefaultInsertStrategy(namedParameterJdbcOperations);
3526
}
3627
}
3728

38-
interface InsertStrategy {
39-
@Nullable
40-
Object execute(String sql, SqlParameterSource sqlParameterSource);
41-
42-
Object[] execute(String sql, SqlParameterSource[] sqlParameterSources);
29+
BatchInsertStrategy batchInsertStrategy(boolean generateIds, @Nullable SqlIdentifier idColumn) {
30+
if (generateIds) {
31+
return new IdGeneratingBatchInsertStrategy(
32+
new IdGeneratingInsertStrategy(dialect, namedParameterJdbcOperations, idColumn),
33+
dialect, batchJdbcOperations, idColumn);
34+
} else {
35+
return new DefaultBatchInsertStrategy(namedParameterJdbcOperations);
36+
}
4337
}
44-
38+
4539
private static class DefaultInsertStrategy implements InsertStrategy {
4640

4741
private final NamedParameterJdbcOperations jdbcOperations;
@@ -55,100 +49,21 @@ public Object execute(String sql, SqlParameterSource sqlParameterSource) {
5549
jdbcOperations.update(sql, sqlParameterSource);
5650
return null;
5751
}
58-
59-
@Override
60-
public Object[] execute(String sql, SqlParameterSource[] sqlParameterSources) {
61-
jdbcOperations.batchUpdate(sql, sqlParameterSources);
62-
return new Object[sqlParameterSources.length];
63-
}
6452
}
65-
66-
private static class IdGeneratingInsertStrategy implements InsertStrategy {
6753

68-
private final Dialect dialect;
54+
private static class DefaultBatchInsertStrategy implements BatchInsertStrategy {
55+
6956
private final NamedParameterJdbcOperations jdbcOperations;
70-
private final BatchJdbcOperations batchJdbcOperations;
71-
private final SqlIdentifier idColumn;
7257

73-
public IdGeneratingInsertStrategy(Dialect dialect, NamedParameterJdbcOperations jdbcOperations,
74-
BatchJdbcOperations batchJdbcOperations, @Nullable SqlIdentifier idColumn) {
75-
this.dialect = dialect;
58+
public DefaultBatchInsertStrategy(NamedParameterJdbcOperations jdbcOperations) {
7659
this.jdbcOperations = jdbcOperations;
77-
this.batchJdbcOperations = batchJdbcOperations;
78-
this.idColumn = idColumn;
79-
}
80-
81-
@Override
82-
public Object execute(String sql, SqlParameterSource sqlParameterSource) {
83-
KeyHolder holder = new GeneratedKeyHolder();
84-
85-
IdGeneration idGeneration = dialect.getIdGeneration();
86-
87-
if (idGeneration.driverRequiresKeyColumnNames()) {
88-
89-
String[] keyColumnNames = getKeyColumnNames();
90-
if (keyColumnNames.length == 0) {
91-
jdbcOperations.update(sql, sqlParameterSource, holder);
92-
} else {
93-
jdbcOperations.update(sql, sqlParameterSource, holder, keyColumnNames);
94-
}
95-
} else {
96-
jdbcOperations.update(sql, sqlParameterSource, holder);
97-
}
98-
99-
try {
100-
// MySQL just returns one value with a special name
101-
return holder.getKey();
102-
} catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) {
103-
// Postgres returns a value for each column
104-
// MS SQL Server returns a value that might be null.
105-
106-
Map<String, Object> keys = holder.getKeys();
107-
if (keys == null || idColumn == null) {
108-
return null;
109-
}
110-
111-
return keys.get(idColumn.getReference(dialect.getIdentifierProcessing()));
112-
}
11360
}
11461

11562
@Override
11663
public Object[] execute(String sql, SqlParameterSource[] sqlParameterSources) {
117-
GeneratedKeyHolder holder = new GeneratedKeyHolder();
118-
119-
IdGeneration idGeneration = dialect.getIdGeneration();
120-
if (idGeneration.driverRequiresKeyColumnNames()) {
121-
122-
String[] keyColumnNames = getKeyColumnNames();
123-
if (keyColumnNames.length == 0) {
124-
batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder);
125-
} else {
126-
batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder, keyColumnNames);
127-
}
128-
} else {
129-
batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder);
130-
}
131-
Object[] ids = new Object[sqlParameterSources.length];
132-
List<Map<String, Object>> keyList = holder.getKeyList();
133-
for (int i = 0; i < keyList.size(); i++) {
134-
Map<String, Object> keys = keyList.get(i);
135-
if (keys.size() > 1) {
136-
if (idColumn != null) {
137-
ids[i] = keys.get(idColumn.getReference(dialect.getIdentifierProcessing()));
138-
}
139-
} else {
140-
ids[i] = keys.entrySet().stream().findFirst() //
141-
.map(Map.Entry::getValue) //
142-
.orElseThrow(() -> new IllegalStateException("KeyHolder contains an empty key list."));
143-
}
144-
}
145-
return ids;
146-
}
147-
148-
private String[] getKeyColumnNames() {
149-
return Optional.ofNullable(idColumn)
150-
.map(idColumn -> new String[]{idColumn.getReference(dialect.getIdentifierProcessing())})
151-
.orElse(new String[0]);
64+
jdbcOperations.batchUpdate(sql, sqlParameterSources);
65+
return new Object[sqlParameterSources.length];
15266
}
15367
}
68+
15469
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,16 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18-
import static java.util.Arrays.*;
1918
import static java.util.Collections.*;
2019
import static org.assertj.core.api.Assertions.*;
2120
import static org.mockito.ArgumentMatchers.*;
2221
import static org.mockito.Mockito.*;
23-
import static org.springframework.data.relational.core.sql.SqlIdentifier.*;
2422

2523
import java.util.ArrayList;
2624
import java.util.HashMap;
2725
import java.util.List;
2826

2927
import org.junit.jupiter.api.BeforeEach;
30-
import org.junit.jupiter.api.Nested;
3128
import org.junit.jupiter.api.Test;
3229
import org.mockito.ArgumentCaptor;
3330
import org.springframework.core.convert.converter.Converter;
@@ -44,7 +41,6 @@
4441
import org.springframework.jdbc.core.RowMapper;
4542
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4643
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
47-
import org.springframework.jdbc.support.KeyHolder;
4844

4945
import lombok.Data;
5046
import lombok.RequiredArgsConstructor;
@@ -94,7 +90,8 @@ public void before() {
9490

9591
when(sqlParametersFactory.getInsert(any(), any(), any()))
9692
.thenReturn(new SqlIdentifierParameterSource(dialect.getIdentifierProcessing()));
97-
when(insertStrategyFactory.insertStrategy(anyBoolean(), any())).thenReturn(mock(InsertStrategyFactory.InsertStrategy.class));
93+
when(insertStrategyFactory.insertStrategy(anyBoolean(), any())).thenReturn(mock(InsertStrategy.class));
94+
when(insertStrategyFactory.batchInsertStrategy(anyBoolean(), any())).thenReturn(mock(BatchInsertStrategy.class));
9895
}
9996

10097
@Test // DATAJDBC-412
@@ -164,23 +161,23 @@ public void batchInsertWithDefinedIdDoesNotRetrieveGeneratedKeys() {
164161

165162
accessStrategy.insert(singletonList(RecordDescriptor.of(new DummyEntity(ORIGINAL_ID), Identifier.from(additionalParameters))), DummyEntity.class, true);
166163

167-
verify(insertStrategyFactory).insertStrategy(false, SqlIdentifier.quoted("ID"));
164+
verify(insertStrategyFactory).batchInsertStrategy(false, SqlIdentifier.quoted("ID"));
168165
}
169166

170167
@Test
171168
public void batchInsertWithUndefinedIdRetrievesGeneratedKeys() {
172169

173170
accessStrategy.insert(singletonList(RecordDescriptor.of(new DummyEntity(ORIGINAL_ID), Identifier.from(additionalParameters))), DummyEntity.class, false);
174171

175-
verify(insertStrategyFactory).insertStrategy(true, SqlIdentifier.quoted("ID"));
172+
verify(insertStrategyFactory).batchInsertStrategy(true, SqlIdentifier.quoted("ID"));
176173
}
177174

178175
@Test
179176
public void batchInsertForEntityWithNoId() {
180177

181178
accessStrategy.insert(singletonList(RecordDescriptor.of(new DummyEntityWithoutIdAnnotation(ORIGINAL_ID), Identifier.from(additionalParameters))), DummyEntityWithoutIdAnnotation.class, false);
182179

183-
verify(insertStrategyFactory).insertStrategy(true, null);
180+
verify(insertStrategyFactory).batchInsertStrategy(true, null);
184181
}
185182

186183
private DefaultDataAccessStrategy createAccessStrategyWithConverter(List<?> converters) {

0 commit comments

Comments
 (0)