From 40df1a955663f880e427b0b76398a1bda065bcf3 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Mon, 7 Feb 2022 10:47:02 -0600 Subject: [PATCH 1/2] 1159-batch-insert-within-aggregate - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 2c64717780..d95d055223 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.4.0-SNAPSHOT + 2.4.0-1159-batch-insert-within-aggregate-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 0646c2846d..b6cd0d8554 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.4.0-SNAPSHOT + 2.4.0-1159-batch-insert-within-aggregate-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 11114a795e..d641c2111b 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.4.0-SNAPSHOT + 2.4.0-1159-batch-insert-within-aggregate-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.4.0-SNAPSHOT + 2.4.0-1159-batch-insert-within-aggregate-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index a6eb48c891..2ce8f79b55 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.4.0-SNAPSHOT + 2.4.0-1159-batch-insert-within-aggregate-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.4.0-SNAPSHOT + 2.4.0-1159-batch-insert-within-aggregate-SNAPSHOT From 26904fc43206d66582e46dfd41ddea504c5db587 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Mon, 7 Feb 2022 12:15:48 -0600 Subject: [PATCH 2/2] Batch inserts of referenced entities when the db supports it. + Group into batches when populating AggregateChange based on the presence/absence of a value for @Id. + Add Dialect IdGeneration property to indicate support for batch operations. No support by SqlServer and DB2 Dialects. --- .../jdbc/core/AggregateChangeExecutor.java | 5 +- .../JdbcAggregateChangeExecutionContext.java | 37 +- .../core/convert/BatchInsertStrategy.java | 36 ++ .../core/convert/BatchJdbcOperations.java | 165 ++++++ .../convert/CascadingDataAccessStrategy.java | 18 +- .../jdbc/core/convert/DataAccessStrategy.java | 44 +- .../convert/DefaultDataAccessStrategy.java | 337 ++---------- .../convert/DelegatingDataAccessStrategy.java | 20 +- .../IdGeneratingBatchInsertStrategy.java | 98 ++++ .../convert/IdGeneratingInsertStrategy.java | 91 ++++ .../jdbc/core/convert/InsertStrategy.java | 38 ++ .../core/convert/InsertStrategyFactory.java | 106 ++++ .../data/jdbc/core/convert/InsertSubject.java | 69 +++ .../data/jdbc/core/convert/SqlGenerator.java | 6 +- .../core/convert/SqlParametersFactory.java | 295 ++++++++++ .../mybatis/MyBatisDataAccessStrategy.java | 40 +- .../config/AbstractJdbcConfiguration.java | 15 +- .../support/JdbcRepositoryFactoryBean.java | 12 +- ...eChangeIdGenerationImmutableUnitTests.java | 10 +- .../AggregateChangeIdGenerationUnitTests.java | 10 +- ...angeExecutorContextImmutableUnitTests.java | 21 +- ...gregateChangeExecutorContextUnitTests.java | 82 ++- .../DefaultDataAccessStrategyUnitTests.java | 251 ++------- .../IdGeneratingBatchInsertStrategyTest.java | 230 ++++++++ .../IdGeneratingInsertStrategyTest.java | 184 +++++++ .../convert/InsertStrategyFactoryTest.java | 65 +++ .../convert/SqlParametersFactoryTest.java | 220 ++++++++ .../MyBatisDataAccessStrategyUnitTests.java | 6 +- ...oryPropertyConversionIntegrationTests.java | 7 +- ...anuallyAssignedIdHsqlIntegrationTests.java | 7 +- ...bcRepositoryWithListsIntegrationTests.java | 53 +- .../SimpleJdbcRepositoryEventsUnitTests.java | 16 +- ...nableJdbcRepositoriesIntegrationTests.java | 9 +- .../data/jdbc/testing/TestConfiguration.java | 16 +- ...epositoryWithListsIntegrationTests-db2.sql | 21 + ...RepositoryWithListsIntegrationTests-h2.sql | 17 + ...positoryWithListsIntegrationTests-hsql.sql | 17 + ...itoryWithListsIntegrationTests-mariadb.sql | 17 + ...ositoryWithListsIntegrationTests-mssql.sql | 20 + ...ositoryWithListsIntegrationTests-mysql.sql | 17 + ...sitoryWithListsIntegrationTests-oracle.sql | 21 + ...toryWithListsIntegrationTests-postgres.sql | 20 + .../relational/core/conversion/DbAction.java | 105 ++-- .../core/conversion/IdValueSource.java | 65 +++ .../core/conversion/WritingContext.java | 52 +- .../relational/core/dialect/Db2Dialect.java | 12 + .../relational/core/dialect/IdGeneration.java | 17 +- .../core/dialect/SqlServerDialect.java | 12 + .../core/conversion/DbActionTestSupport.java | 15 + .../RelationalEntityWriterUnitTests.java | 506 ++++++++++++++---- 50 files changed, 2757 insertions(+), 796 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchInsertStrategy.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchJdbcOperations.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategy.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategy.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategy.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertSubject.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategyTest.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategyTest.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactoryTest.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java index ed227fe34a..85b03407ea 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -28,6 +28,7 @@ * * @author Jens Schauder * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 2.0 */ class AggregateChangeExecutor { @@ -66,6 +67,8 @@ private void execute(DbAction action, JdbcAggregateChangeExecutionContext exe executionContext.executeInsertRoot((DbAction.InsertRoot) action); } else if (action instanceof DbAction.Insert) { executionContext.executeInsert((DbAction.Insert) action); + } else if (action instanceof DbAction.InsertBatch) { + executionContext.executeInsertBatch((DbAction.InsertBatch) action); } else if (action instanceof DbAction.UpdateRoot) { executionContext.executeUpdateRoot((DbAction.UpdateRoot) action); } else if (action instanceof DbAction.Update) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java index 522af4f28e..1c64958e03 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 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. @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; @@ -32,6 +33,7 @@ import org.springframework.data.jdbc.core.convert.Identifier; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; +import org.springframework.data.jdbc.core.convert.InsertSubject; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; @@ -51,6 +53,7 @@ * @author Jens Schauder * @author Umut Erturk * @author Myeonghyeon Lee + * @author Chirag Tailor */ class JdbcAggregateChangeExecutionContext { @@ -87,11 +90,11 @@ void executeInsertRoot(DbAction.InsertRoot insert) { T rootEntity = RelationalEntityVersionUtils.setVersionNumberOnEntity( // insert.getEntity(), initialVersion, persistentEntity, converter); - id = accessStrategy.insert(rootEntity, insert.getEntityType(), Identifier.empty()); + id = accessStrategy.insert(rootEntity, insert.getEntityType(), Identifier.empty(), insert.getIdValueSource()); setNewVersion(initialVersion); } else { - id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty()); + id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(), insert.getIdValueSource()); } add(new DbActionExecutionResult(insert, id)); @@ -100,10 +103,24 @@ void executeInsertRoot(DbAction.InsertRoot insert) { void executeInsert(DbAction.Insert insert) { Identifier parentKeys = getParentKeys(insert, converter); - Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys); + Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys, insert.getIdValueSource()); add(new DbActionExecutionResult(insert, id)); } + void executeInsertBatch(DbAction.InsertBatch insertBatch) { + + List> inserts = insertBatch.getInserts(); + List> insertSubjects = inserts.stream() + .map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))) + .collect(Collectors.toList()); + + Object[] ids = accessStrategy.insert(insertSubjects, insertBatch.getEntityType(), insertBatch.getIdValueSource()); + + for (int i = 0; i < inserts.size(); i++) { + add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null)); + } + } + void executeUpdateRoot(DbAction.UpdateRoot update) { RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(update.getEntityType()); @@ -155,18 +172,6 @@ void executeDeleteAll(DbAction.DeleteAll delete) { accessStrategy.deleteAll(delete.getPropertyPath()); } - void executeMerge(DbAction.Merge merge) { - - // temporary implementation - if (!accessStrategy.update(merge.getEntity(), merge.getEntityType())) { - - Object id = accessStrategy.insert(merge.getEntity(), merge.getEntityType(), getParentKeys(merge, converter)); - add(new DbActionExecutionResult(merge, id)); - } else { - add(new DbActionExecutionResult()); - } - } - void executeAcquireLock(DbAction.AcquireLockRoot acquireLock) { accessStrategy.acquireLockById(acquireLock.getId(), LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType()); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchInsertStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchInsertStrategy.java new file mode 100644 index 0000000000..2d01cfe6b9 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchInsertStrategy.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * Strategy for executing a batch insert. + * + * @author Chirag Tailor + * @since 2.4 + */ +interface BatchInsertStrategy { + + /** + * @param sql the insert sql. Must not be {@code null}. + * @param sqlParameterSources the sql parameters for each record to be inserted. Must not be {@code null}. + * @return the ids corresponding to each record that was inserted, if ids were generated. If ids were not generated, + * elements will be {@code null}. + * @since 2.4 + */ + Object[] execute(String sql, SqlParameterSource[] sqlParameterSources); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchJdbcOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchJdbcOperations.java new file mode 100644 index 0000000000..0a2cfbc0cf --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BatchJdbcOperations.java @@ -0,0 +1,165 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementCreatorFactory; +import org.springframework.jdbc.core.RowMapperResultSetExtractor; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Counterpart to {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations} containing + * methods for performing batch updates with generated keys. + * + * @author Chirag Tailor + * @since 2.4 + */ +public class BatchJdbcOperations { + private final JdbcOperations jdbcOperations; + + public BatchJdbcOperations(JdbcOperations jdbcOperations) { + this.jdbcOperations = jdbcOperations; + } + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments, + * returning generated keys. + * @param sql the SQL statement to execute + * @param batchArgs the array of {@link SqlParameterSource} containing the batch of + * arguments for the query + * @param generatedKeyHolder a {@link KeyHolder} that will hold the generated keys + * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws org.springframework.dao.DataAccessException if there is any problem issuing the update + * @see org.springframework.jdbc.support.GeneratedKeyHolder + * @since 2.4 + */ + int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder generatedKeyHolder) { + return batchUpdate(sql, batchArgs, generatedKeyHolder, null); + } + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments, + * returning generated keys. + * @param sql the SQL statement to execute + * @param batchArgs the array of {@link SqlParameterSource} containing the batch of + * arguments for the query + * @param generatedKeyHolder a {@link KeyHolder} that will hold the generated keys + * @param keyColumnNames names of the columns that will have keys generated for them + * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws org.springframework.dao.DataAccessException if there is any problem issuing the update + * @see org.springframework.jdbc.support.GeneratedKeyHolder + * @since 2.4 + */ + int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder generatedKeyHolder, + @Nullable String[] keyColumnNames) { + + if (batchArgs.length == 0) { + return new int[0]; + } + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + SqlParameterSource paramSource = batchArgs[0]; + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + List declaredParameters = NamedParameterUtils.buildSqlParameterList(parsedSql, paramSource); + PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(sqlToUse, declaredParameters); + if (keyColumnNames != null) { + pscf.setGeneratedKeysColumnNames(keyColumnNames); + } else { + pscf.setReturnGeneratedKeys(true); + } + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + PreparedStatementCreator psc = pscf.newPreparedStatementCreator(params); + BatchPreparedStatementSetter bpss = new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); + pscf.newPreparedStatementSetter(values).setValues(ps); + } + + @Override + public int getBatchSize() { + return batchArgs.length; + } + }; + PreparedStatementCallback preparedStatementCallback = ps -> { + int batchSize = bpss.getBatchSize(); + generatedKeyHolder.getKeyList().clear(); + if (JdbcUtils.supportsBatchUpdates(ps.getConnection())) { + for (int i = 0; i < batchSize; i++) { + bpss.setValues(ps, i); + ps.addBatch(); + } + int[] results = ps.executeBatch(); + storeGeneratedKeys(generatedKeyHolder, ps, batchSize); + return results; + } else { + List rowsAffected = new ArrayList<>(); + for (int i = 0; i < batchSize; i++) { + bpss.setValues(ps, i); + rowsAffected.add(ps.executeUpdate()); + storeGeneratedKeys(generatedKeyHolder, ps, 1); + } + int[] rowsAffectedArray = new int[rowsAffected.size()]; + for (int i = 0; i < rowsAffectedArray.length; i++) { + rowsAffectedArray[i] = rowsAffected.get(i); + } + return rowsAffectedArray; + } + }; + int[] result = jdbcOperations.execute(psc, preparedStatementCallback); + Assert.state(result != null, "No result array"); + return result; + } + + private void storeGeneratedKeys(KeyHolder generatedKeyHolder, PreparedStatement ps, int rowsExpected) throws SQLException { + + List> generatedKeys = generatedKeyHolder.getKeyList(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys != null) { + try { + RowMapperResultSetExtractor> rse = + new RowMapperResultSetExtractor<>(new ColumnMapRowMapper(), rowsExpected); + //noinspection ConstantConditions + generatedKeys.addAll(rse.extractData(keys)); + } + finally { + JdbcUtils.closeResultSet(keys); + } + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java index e24b3bf6ce..14ea636f7e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -23,6 +23,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.LockMode; @@ -35,6 +36,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 1.1 */ public class CascadingDataAccessStrategy implements DataAccessStrategy { @@ -45,13 +47,23 @@ public CascadingDataAccessStrategy(List strategies) { this.strategies = new ArrayList<>(strategies); } + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + return collect(das -> das.insert(instance, domainType, identifier)); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, org.springframework.data.jdbc.core.ParentKeys) */ @Override - public Object insert(T instance, Class domainType, Identifier identifier) { - return collect(das -> das.insert(instance, domainType, identifier)); + public Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { + return collect(das -> das.insert(instance, domainType, identifier, idValueSource)); + } + + @Override + public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { + return collect(das -> das.insert(insertSubjects, domainType, idValueSource)); } /* diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java index 78bb984c06..2f6d508b81 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.List; import java.util.Map; import org.springframework.dao.OptimisticLockingFailureException; @@ -22,6 +23,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.lang.Nullable; @@ -35,24 +37,58 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor */ public interface DataAccessStrategy extends RelationResolver { /** - * Inserts a the data of a single entity. Referenced entities don't get handled. + * Inserts the data of a single entity. Referenced entities don't get handled. * + * @param the type of the instance. * @param instance the instance to be stored. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @param identifier information about data that needs to be considered for the insert but which is not part of the * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a - * {@link Map} or {@link java.util.List}. - * @param the type of the instance. + * {@link Map} or {@link List}. * @return the id generated by the database if any. * @since 1.1 + * @deprecated since 2.4, use {@link #insert(Object, Class, Identifier, IdValueSource)}. This will no longer insert as + * expected when the id property of the instance is pre-populated. */ @Nullable + @Deprecated Object insert(T instance, Class domainType, Identifier identifier); + /** + * Inserts the data of a single entity. Referenced entities don't get handled. + * + * @param the type of the instance. + * @param instance the instance to be stored. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param identifier information about data that needs to be considered for the insert but which is not part of the + * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a + * {@link Map} or {@link List}. + * @param idValueSource the {@link IdValueSource} for the insert. + * @return the id generated by the database if any. + * @since 2.4 + */ + @Nullable + Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource); + + /** + * Inserts the data of a multiple entities. + * + * @param the type of the instance. + * @param insertSubjects the subjects to be inserted, where each subject contains the instance and its identifier. + * Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param idValueSource the {@link IdValueSource} for the insert. + * @return the ids corresponding to each record that was inserted, if ids were generated. If ids were not generated, + * elements will be {@code null}. + * @since 2.4 + */ + Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource); + /** * Updates the data of a single entity in the database. Referenced entities don't get handled. * diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index 151c77027a..043f9ee402 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -18,26 +18,16 @@ import static org.springframework.data.jdbc.core.convert.SqlGenerator.*; import java.sql.ResultSet; -import java.sql.SQLType; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.function.Predicate; +import java.util.Optional; -import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.mapping.JdbcValue; -import org.springframework.data.jdbc.support.JdbcUtil; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.relational.core.dialect.IdGeneration; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -48,9 +38,6 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.jdbc.support.KeyHolder; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -68,6 +55,7 @@ * @author Myeonghyeon Lee * @author Yunyoung LEE * @author Radim Tlusty + * @author Chirag Tailor * @since 1.1 */ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -76,6 +64,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private final RelationalMappingContext context; private final JdbcConverter converter; private final NamedParameterJdbcOperations operations; + private final SqlParametersFactory sqlParametersFactory; + private final InsertStrategyFactory insertStrategyFactory; /** * Creates a {@link DefaultDataAccessStrategy} @@ -87,17 +77,28 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { * @since 1.1 */ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, RelationalMappingContext context, - JdbcConverter converter, NamedParameterJdbcOperations operations) { - + JdbcConverter converter, NamedParameterJdbcOperations operations, SqlParametersFactory sqlParametersFactory, + InsertStrategyFactory insertStrategyFactory) { Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); Assert.notNull(context, "RelationalMappingContext must not be null"); Assert.notNull(converter, "JdbcConverter must not be null"); Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); + Assert.notNull(sqlParametersFactory, "SqlParametersFactory must not be null"); + Assert.notNull(insertStrategyFactory, "InsertStrategyFactory must not be null"); this.sqlGeneratorSource = sqlGeneratorSource; this.context = context; this.converter = converter; this.operations = operations; + this.sqlParametersFactory = sqlParametersFactory; + this.insertStrategyFactory = insertStrategyFactory; + } + + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + + RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(domainType); + return insert(instance, domainType, identifier, IdValueSource.forInstance(instance, persistentEntity)); } /* @@ -105,54 +106,26 @@ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, Relation * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) */ @Override - public Object insert(T instance, Class domainType, Identifier identifier) { - - SqlGenerator sqlGenerator = sql(domainType); - RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - - SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", - PersistentProperty::isIdProperty, getIdentifierProcessing()); - - identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); - - Object idValue = getIdValueOrNull(instance, persistentEntity); - if (idValue != null) { - - RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); - } + public Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { - String insertSql = sqlGenerator.getInsert(new HashSet<>(parameterSource.getIdentifiers())); + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forInsert(instance, domainType, identifier, idValueSource); - if (idValue == null) { - return executeInsertAndReturnGeneratedId(domainType, persistentEntity, parameterSource, insertSql); - } else { + String insertSql = sql(domainType).getInsert(parameterSource.getIdentifiers()); - operations.update(insertSql, parameterSource); - return null; - } + return insertStrategyFactory.insertStrategy(idValueSource, getIdColumn(domainType)).execute(insertSql, parameterSource); } - @Nullable - private Object executeInsertAndReturnGeneratedId(Class domainType, RelationalPersistentEntity persistentEntity, SqlIdentifierParameterSource parameterSource, String insertSql) { - - KeyHolder holder = new GeneratedKeyHolder(); - - IdGeneration idGeneration = sqlGeneratorSource.getDialect().getIdGeneration(); + @Override + public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { - if (idGeneration.driverRequiresKeyColumnNames()) { + Assert.notEmpty(insertSubjects, "Batch insert must contain at least one InsertSubject"); + SqlIdentifierParameterSource[] sqlParameterSources = insertSubjects.stream() + .map(insertSubject -> sqlParametersFactory.forInsert(insertSubject.getInstance(), domainType, insertSubject.getIdentifier(), idValueSource)) + .toArray(SqlIdentifierParameterSource[]::new); - String[] keyColumnNames = getKeyColumnNames(domainType); - if (keyColumnNames.length == 0) { - operations.update(insertSql, parameterSource, holder); - } else { - operations.update(insertSql, parameterSource, holder, keyColumnNames); - } - } else { - operations.update(insertSql, parameterSource, holder); - } + String insertSql = sql(domainType).getInsert(sqlParameterSources[0].getIdentifiers()); - return getIdFromHolder(holder, persistentEntity); + return insertStrategyFactory.batchInsertStrategy(idValueSource, getIdColumn(domainType)).execute(insertSql, sqlParameterSources); } /* @@ -161,10 +134,7 @@ private Object executeInsertAndReturnGeneratedId(Class domainType, Relati */ @Override public boolean update(S instance, Class domainType) { - - RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - return operations.update(sql(domainType).getUpdate(), - getParameterSource(instance, persistentEntity, "", Predicates.includeAll(), getIdentifierProcessing())) != 0; + return operations.update(sql(domainType).getUpdate(), sqlParametersFactory.forUpdate(instance, domainType)) != 0; } /* @@ -177,8 +147,7 @@ public boolean updateWithVersion(S instance, Class domainType, Number pre RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); // Adjust update statement to set the new version and use the old version in where clause. - SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", - Predicates.includeAll(), getIdentifierProcessing()); + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forUpdate(instance, domainType); parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion); int affectedRows = operations.update(sql(domainType).getUpdateWithVersion(), parameterSource); @@ -200,7 +169,7 @@ public boolean updateWithVersion(S instance, Class domainType, Number pre public void delete(Object id, Class domainType) { String deleteByIdSql = sql(domainType).getDeleteById(); - SqlParameterSource parameter = createIdParameterSource(id, domainType); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); operations.update(deleteByIdSql, parameter); } @@ -216,7 +185,7 @@ public void deleteWithVersion(Object id, Class domainType, Number previou RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - SqlIdentifierParameterSource parameterSource = createIdParameterSource(id, domainType); + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion); int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource); @@ -241,13 +210,7 @@ public void delete(Object rootId, PersistentPropertyPath prope public void acquireLockById(Object id, LockMode lockMode, Class domainType) { String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode); - SqlIdentifierParameterSource parameter = createIdParameterSource(id, domainType); + SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); operations.query(acquireLockByIdSql, parameter, ResultSet::next); } @@ -317,7 +280,7 @@ public long count(Class domainType) { public T findById(Object id, Class domainType) { String findOneSql = sql(domainType).getFindOne(); - SqlIdentifierParameterSource parameter = createIdParameterSource(id, domainType); + SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); try { return operations.queryForObject(findOneSql, parameter, (RowMapper) getEntityRowMapper(domainType)); @@ -348,10 +311,7 @@ public Iterable findAllById(Iterable ids, Class domainType) { return Collections.emptyList(); } - RelationalPersistentProperty idProperty = getRequiredPersistentEntity(domainType).getRequiredIdProperty(); - SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(getIdentifierProcessing()); - - addConvertedPropertyValuesAsList(parameterSource, idProperty, ids, IDS_SQL_PARAMETER); + SqlParameterSource parameterSource = sqlParametersFactory.forQueryByIds(ids, domainType); String findAllInListSql = sql(domainType).getFindAllInList(); @@ -379,18 +339,8 @@ public Iterable findAllByPath(Identifier identifier, RowMapper rowMapper = path.isMap() ? this.getMapEntityRowMapper(path, identifier) : this.getEntityRowMapper(path, identifier); - return operations.query(findAllByProperty, createParameterSource(identifier, getIdentifierProcessing()), - (RowMapper) rowMapper); - } - - private SqlParameterSource createParameterSource(Identifier identifier, IdentifierProcessing identifierProcessing) { - - SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(identifierProcessing); - - identifier.toMap() - .forEach((name, value) -> addConvertedPropertyValue(parameterSource, name, value, value.getClass())); - - return parameterSource; + SqlParameterSource parameterSource = sqlParametersFactory.forQueryByIdentifier(identifier); + return operations.query(findAllByProperty, parameterSource, (RowMapper) rowMapper); } /* @@ -401,7 +351,7 @@ private SqlParameterSource createParameterSource(Identifier identifier, Identifi public boolean existsById(Object id, Class domainType) { String existsSql = sql(domainType).getExists(); - SqlParameterSource parameter = createIdParameterSource(id, domainType); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class); Assert.state(result != null, "The result of an exists query must not be null"); @@ -429,87 +379,6 @@ public Iterable findAll(Class domainType, Pageable pageable) { return operations.query(sql(domainType).getFindAll(pageable), (RowMapper) getEntityRowMapper(domainType)); } - private SqlIdentifierParameterSource getParameterSource(@Nullable S instance, - RelationalPersistentEntity persistentEntity, String prefix, - Predicate skipProperty, IdentifierProcessing identifierProcessing) { - - SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(identifierProcessing); - - PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) - : NoValuePropertyAccessor.instance(); - - persistentEntity.doWithAll(property -> { - - if (skipProperty.test(property) || !property.isWritable()) { - return; - } - if (property.isEntity() && !property.isEmbedded()) { - return; - } - - if (property.isEmbedded()) { - - Object value = propertyAccessor.getProperty(property); - RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); - SqlIdentifierParameterSource additionalParameters = getParameterSource((T) value, - (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty, - identifierProcessing); - parameters.addAll(additionalParameters); - } else { - - Object value = propertyAccessor.getProperty(property); - SqlIdentifier paramName = property.getColumnName().transform(prefix::concat); - - addConvertedPropertyValue(parameters, property, value, paramName); - } - }); - - return parameters; - } - - /** - * Returns the id value if its not a primitive zero. Returns {@literal null} if the id value is null or a primitive - * zero. - */ - @Nullable - @SuppressWarnings("unchecked") - private ID getIdValueOrNull(S instance, RelationalPersistentEntity persistentEntity) { - - ID idValue = (ID) persistentEntity.getIdentifierAccessor(instance).getIdentifier(); - - return isIdPropertyNullOrScalarZero(idValue, persistentEntity) ? null : idValue; - } - - private static boolean isIdPropertyNullOrScalarZero(@Nullable ID idValue, - RelationalPersistentEntity persistentEntity) { - - RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); - return idValue == null // - || idProperty == null // - || (idProperty.getType() == int.class && idValue.equals(0)) // - || (idProperty.getType() == long.class && idValue.equals(0L)); - } - - @Nullable - private Object getIdFromHolder(KeyHolder holder, RelationalPersistentEntity persistentEntity) { - - try { - // MySQL just returns one value with a special name - return holder.getKey(); - } catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) { - // Postgres returns a value for each column - // MS SQL Server returns a value that might be null. - - Map keys = holder.getKeys(); - - if (keys == null || persistentEntity.getIdProperty() == null) { - return null; - } - - return keys.get(persistentEntity.getIdColumn().getReference(getIdentifierProcessing())); - } - } - private EntityRowMapper getEntityRowMapper(Class domainType) { return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter); } @@ -526,72 +395,10 @@ private RowMapper getMapEntityRowMapper(PersistentPropertyPathExtension path, return new MapEntityRowMapper<>(path, converter, identifier, keyColumn, getIdentifierProcessing()); } - private SqlIdentifierParameterSource createIdParameterSource(Object id, Class domainType) { - - SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(getIdentifierProcessing()); - - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - ID_SQL_PARAMETER // - ); - return parameterSource; - } - private IdentifierProcessing getIdentifierProcessing() { return sqlGeneratorSource.getDialect().getIdentifierProcessing(); } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, - RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { - - addConvertedValue(parameterSource, value, name, converter.getColumnType(property), converter.getTargetSqlType(property)); - } - - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, SqlIdentifier name, Object value, - Class javaType) { - - addConvertedValue(parameterSource, value, name, javaType, JdbcUtil.targetSqlTypeFor(javaType)); - } - - private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, - SqlIdentifier paramName, Class javaType, SQLType sqlType) { - - JdbcValue jdbcValue = converter.writeJdbcValue( // - value, // - javaType, // - sqlType // - ); - - parameterSource.addValue( // - paramName, // - jdbcValue.getValue(), // - jdbcValue.getJdbcType().getVendorTypeNumber()); - } - - private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, - RelationalPersistentProperty property, Iterable values, SqlIdentifier paramName) { - - List convertedIds = new ArrayList<>(); - JdbcValue jdbcValue = null; - for (Object id : values) { - - Class columnType = converter.getColumnType(property); - SQLType sqlType = converter.getTargetSqlType(property); - - jdbcValue = converter.writeJdbcValue(id, columnType, sqlType); - convertedIds.add(jdbcValue.getValue()); - } - - Assert.state(jdbcValue != null, "JdbcValue must be not null at this point. Please report this as a bug."); - - SQLType jdbcType = jdbcValue.getJdbcType(); - int typeNumber = jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); - - parameterSource.addValue(paramName, convertedIds, typeNumber); - } - @SuppressWarnings("unchecked") private RelationalPersistentEntity getRequiredPersistentEntity(Class domainType) { return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); @@ -601,60 +408,10 @@ private SqlGenerator sql(Class domainType) { return sqlGeneratorSource.getSqlGenerator(domainType); } - private String[] getKeyColumnNames(Class domainType) { - - RelationalPersistentEntity requiredPersistentEntity = context.getRequiredPersistentEntity(domainType); - - if (!requiredPersistentEntity.hasIdProperty()) { - return new String[0]; - } - - SqlIdentifier idColumn = requiredPersistentEntity.getIdColumn(); - - return new String[] { idColumn.getReference(getIdentifierProcessing()) }; - } - - /** - * Utility to create {@link Predicate}s. - */ - static class Predicates { - - /** - * Include all {@link Predicate} returning {@literal false} to never skip a property. - * - * @return the include all {@link Predicate}. - */ - static Predicate includeAll() { - return it -> false; - } - } - - /** - * A {@link PersistentPropertyAccessor} implementation always returning null - * - * @param - */ - static class NoValuePropertyAccessor implements PersistentPropertyAccessor { - - private static final NoValuePropertyAccessor INSTANCE = new NoValuePropertyAccessor(); - - static NoValuePropertyAccessor instance() { - return INSTANCE; - } - - @Override - public void setProperty(PersistentProperty property, @Nullable Object value) { - throw new UnsupportedOperationException("Cannot set value on 'null' target object."); - } - - @Override - public Object getProperty(PersistentProperty property) { - return null; - } - - @Override - public T getBean() { - return null; - } + @Nullable + private SqlIdentifier getIdColumn(Class domainType) { + return Optional.ofNullable(context.getRequiredPersistentEntity(domainType).getIdProperty()) + .map(RelationalPersistentProperty::getColumnName) + .orElse(null); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java index 1a917409c4..490a7acbbb 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -18,10 +18,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.util.Assert; +import java.util.List; + /** * Delegates all method calls to an instance set after construction. This is useful for {@link DataAccessStrategy}s with * cyclic dependencies. @@ -30,19 +33,30 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 1.1 */ public class DelegatingDataAccessStrategy implements DataAccessStrategy { private DataAccessStrategy delegate; + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + return delegate.insert(instance, domainType, identifier); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, org.springframework.data.jdbc.core.ParentKeys) */ @Override - public Object insert(T instance, Class domainType, Identifier identifier) { - return delegate.insert(instance, domainType, identifier); + public Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { + return delegate.insert(instance, domainType, identifier, idValueSource); + } + + @Override + public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { + return delegate.insert(insertSubjects, domainType, idValueSource); } /* diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategy.java new file mode 100644 index 0000000000..296f53c27a --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategy.java @@ -0,0 +1,98 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.IdGeneration; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.lang.Nullable; + +/** + * A {@link BatchInsertStrategy} that expects ids to be generated from the batch insert. When the {@link Dialect} does + * not support id generation for batch operations, this implementation falls back to performing the inserts serially. + * + * @author Chirag Tailor + * @since 2.4 + */ +class IdGeneratingBatchInsertStrategy implements BatchInsertStrategy { + + private final InsertStrategy insertStrategy; + private final Dialect dialect; + private final BatchJdbcOperations batchJdbcOperations; + private final SqlIdentifier idColumn; + + public IdGeneratingBatchInsertStrategy(InsertStrategy insertStrategy, + Dialect dialect, BatchJdbcOperations batchJdbcOperations, + @Nullable SqlIdentifier idColumn) { + this.insertStrategy = insertStrategy; + this.dialect = dialect; + this.batchJdbcOperations = batchJdbcOperations; + this.idColumn = idColumn; + } + + @Override + public Object[] execute(String sql, SqlParameterSource[] sqlParameterSources) { + + if (!dialect.getIdGeneration().supportedForBatchOperations()) { + return Arrays.stream(sqlParameterSources) + .map(sqlParameterSource -> insertStrategy.execute(sql, sqlParameterSource)) + .toArray(); + } + + GeneratedKeyHolder holder = new GeneratedKeyHolder(); + IdGeneration idGeneration = dialect.getIdGeneration(); + if (idGeneration.driverRequiresKeyColumnNames()) { + + String[] keyColumnNames = getKeyColumnNames(); + if (keyColumnNames.length == 0) { + batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder); + } else { + batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder, keyColumnNames); + } + } else { + batchJdbcOperations.batchUpdate(sql, sqlParameterSources, holder); + } + Object[] ids = new Object[sqlParameterSources.length]; + List> keyList = holder.getKeyList(); + for (int i = 0; i < keyList.size(); i++) { + Map keys = keyList.get(i); + if (keys.size() > 1) { + if (idColumn != null) { + ids[i] = keys.get(idColumn.getReference(dialect.getIdentifierProcessing())); + } + } else { + ids[i] = keys.entrySet().stream().findFirst() // + .map(Map.Entry::getValue) // + .orElseThrow(() -> new IllegalStateException("KeyHolder contains an empty key list.")); + } + } + return ids; + } + + private String[] getKeyColumnNames() { + + return Optional.ofNullable(idColumn) + .map(idColumn -> new String[]{idColumn.getReference(dialect.getIdentifierProcessing())}) + .orElse(new String[0]); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategy.java new file mode 100644 index 0000000000..5577b72cfa --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategy.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.IdGeneration; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.lang.Nullable; + +import java.util.Map; +import java.util.Optional; + +/** + * An {@link InsertStrategy} that expects an id to be generated from the insert. + * + * @author Chirag Tailor + * @since 2.4 + */ +class IdGeneratingInsertStrategy implements InsertStrategy { + + private final Dialect dialect; + private final NamedParameterJdbcOperations jdbcOperations; + private final SqlIdentifier idColumn; + + public IdGeneratingInsertStrategy(Dialect dialect, NamedParameterJdbcOperations jdbcOperations, + @Nullable SqlIdentifier idColumn) { + this.dialect = dialect; + this.jdbcOperations = jdbcOperations; + this.idColumn = idColumn; + } + + @Override + public Object execute(String sql, SqlParameterSource sqlParameterSource) { + + KeyHolder holder = new GeneratedKeyHolder(); + + IdGeneration idGeneration = dialect.getIdGeneration(); + + if (idGeneration.driverRequiresKeyColumnNames()) { + + String[] keyColumnNames = getKeyColumnNames(); + if (keyColumnNames.length == 0) { + jdbcOperations.update(sql, sqlParameterSource, holder); + } else { + jdbcOperations.update(sql, sqlParameterSource, holder, keyColumnNames); + } + } else { + jdbcOperations.update(sql, sqlParameterSource, holder); + } + + try { +// MySQL just returns one value with a special name + return holder.getKey(); + } catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) { + // Postgres returns a value for each column + // MS SQL Server returns a value that might be null. + + Map keys = holder.getKeys(); + if (keys == null || idColumn == null) { + return null; + } + + return keys.get(idColumn.getReference(dialect.getIdentifierProcessing())); + } + } + + private String[] getKeyColumnNames() { + return Optional.ofNullable(idColumn) + .map(idColumn -> new String[]{idColumn.getReference(dialect.getIdentifierProcessing())}) + .orElse(new String[0]); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategy.java new file mode 100644 index 0000000000..2bd19e043d --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategy.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.lang.Nullable; + +/** + * Strategy for executing an insert. + * + * @author Chirag Tailor + * @since 2.4 + */ +interface InsertStrategy { + + /** + * @param sql the insert sql. Must not be {@code null}. + * @param sqlParameterSource the sql parameters for the record to be inserted. Must not be {@code null}. + * @return the id corresponding to the record that was inserted, if one was generated. If an id was not generated, + * this will be {@code null}. + * @since 2.4 + */ + @Nullable + Object execute(String sql, SqlParameterSource sqlParameterSource); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactory.java new file mode 100644 index 0000000000..7eaad1676d --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.lang.Nullable; + +/** + * Factory which selects and builds the appropriate {@link InsertStrategy} or {@link BatchInsertStrategy} based on + * whether the insert is expected to generate ids. + * + * @author Chirag Tailor + * @since 2.4 + */ +public class InsertStrategyFactory { + private final NamedParameterJdbcOperations namedParameterJdbcOperations; + private final BatchJdbcOperations batchJdbcOperations; + private final Dialect dialect; + + public InsertStrategyFactory(NamedParameterJdbcOperations namedParameterJdbcOperations, BatchJdbcOperations batchJdbcOperations, + Dialect dialect) { + this.namedParameterJdbcOperations = namedParameterJdbcOperations; + this.batchJdbcOperations = batchJdbcOperations; + this.dialect = dialect; + } + + /** + * @param idValueSource the {@link IdValueSource} for the insert. + * @param idColumn the identifier for the id, if an id is expected to be generated. May be {@code null}. + * @return the {@link InsertStrategy} to be used for the insert. + * @since 2.4 + */ + InsertStrategy insertStrategy(IdValueSource idValueSource, @Nullable SqlIdentifier idColumn) { + + if (IdValueSource.GENERATED.equals(idValueSource)) { + return new IdGeneratingInsertStrategy(dialect, namedParameterJdbcOperations, idColumn); + } + return new DefaultInsertStrategy(namedParameterJdbcOperations); + } + + /** + * @param idValueSource the {@link IdValueSource} for the insert. + * @param idColumn the identifier for the id, if an ids are expected to be generated. May be {@code null}. + * @return the {@link BatchInsertStrategy} to be used for the batch insert. + * @since 2.4 + */ + BatchInsertStrategy batchInsertStrategy(IdValueSource idValueSource, @Nullable SqlIdentifier idColumn) { + + if (IdValueSource.GENERATED.equals(idValueSource)) { + return new IdGeneratingBatchInsertStrategy( + new IdGeneratingInsertStrategy(dialect, namedParameterJdbcOperations, idColumn), + dialect, batchJdbcOperations, idColumn); + } + return new DefaultBatchInsertStrategy(namedParameterJdbcOperations); + } + + private static class DefaultInsertStrategy implements InsertStrategy { + + private final NamedParameterJdbcOperations jdbcOperations; + + public DefaultInsertStrategy(NamedParameterJdbcOperations jdbcOperations) { + this.jdbcOperations = jdbcOperations; + } + + @Override + public Object execute(String sql, SqlParameterSource sqlParameterSource) { + + jdbcOperations.update(sql, sqlParameterSource); + return null; + } + } + + private static class DefaultBatchInsertStrategy implements BatchInsertStrategy { + + private final NamedParameterJdbcOperations jdbcOperations; + + public DefaultBatchInsertStrategy(NamedParameterJdbcOperations jdbcOperations) { + this.jdbcOperations = jdbcOperations; + } + + @Override + public Object[] execute(String sql, SqlParameterSource[] sqlParameterSources) { + + jdbcOperations.batchUpdate(sql, sqlParameterSources); + return new Object[sqlParameterSources.length]; + } + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertSubject.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertSubject.java new file mode 100644 index 0000000000..155bd3fb3c --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/InsertSubject.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import java.util.Objects; + +/** + * The subject of an insert, described by the entity instance and its {@link Identifier}, where identifier contains + * information about data that needs to be considered for the insert but which is not part of the entity. Namely + * references back to a parent entity and key/index columns for entities that are stored in a {@link java.util.Map} or + * {@link java.util.List}. + * + * @author Chirag Tailor + * @since 2.4 + */ +public final class InsertSubject { + private final T instance; + private final Identifier identifier; + + public static InsertSubject describedBy(T instance, Identifier identifier) { + return new InsertSubject<>(instance, identifier); + } + + private InsertSubject(T instance, Identifier identifier) { + this.instance = instance; + this.identifier = identifier; + } + + public T getInstance() { + return instance; + } + + public Identifier getIdentifier() { + return identifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + InsertSubject that = (InsertSubject) o; + return Objects.equals(instance, that.instance) && Objects.equals(identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(instance, identifier); + } + + @Override + public String toString() { + return "InsertSubject{" + "instance=" + instance + ", identifier=" + identifier + '}'; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index a95c9b1933..989d734595 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -579,12 +579,16 @@ private String createInsertSql(Set additionalColumns) { insert = insert.column(table.column(cn)); } + if (columnNamesForInsert.isEmpty()) { + return render(insert.build()); + } + InsertBuilder.InsertValuesWithBuild insertWithValues = null; for (SqlIdentifier cn : columnNamesForInsert) { insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); } - return render(insertWithValues == null ? insert.build() : insertWithValues.build()); + return render(insertWithValues.build()); } private String createUpdateSql() { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java new file mode 100644 index 0000000000..a182e44fc1 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -0,0 +1,295 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import static org.springframework.data.jdbc.core.convert.SqlGenerator.*; + +import java.sql.SQLType; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import org.springframework.data.jdbc.core.mapping.JdbcValue; +import org.springframework.data.jdbc.support.JdbcUtil; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Creates the {@link SqlIdentifierParameterSource} for various SQL operations, dialect identifier processing rules and + * applicable converters. + * + * @author Jens Schauder + * @author Chirag Tailor + * @since 2.4 + */ +public class SqlParametersFactory { + private final RelationalMappingContext context; + private final JdbcConverter converter; + private final Dialect dialect; + + public SqlParametersFactory(RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { + this.context = context; + this.converter = converter; + this.dialect = dialect; + } + + /** + * Creates the parameters for a SQL insert operation. + * + * @param instance the entity to be inserted. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param identifier information about data that needs to be considered for the insert but which is not part of the + * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a + * {@link Map} or {@link List}. + * @param idValueSource the {@link IdValueSource} for the insert. + * @return the {@link SqlIdentifierParameterSource} for the insert. Guaranteed to not be {@code null}. + * @since 2.4 + */ + SqlIdentifierParameterSource forInsert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { + + RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", + PersistentProperty::isIdProperty, dialect.getIdentifierProcessing()); + + identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); + + if (IdValueSource.PROVIDED.equals(idValueSource)) { + + RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); + Object idValue = persistentEntity.getIdentifierAccessor(instance).getRequiredIdentifier(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + } + return parameterSource; + } + + /** + * Creates the parameters for a SQL update operation. + * + * @param instance the entity to be updated. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @return the {@link SqlIdentifierParameterSource} for the update. Guaranteed to not be {@code null}. + * @since 2.4 + */ + SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { + + return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", Predicates.includeAll(), + dialect.getIdentifierProcessing()); + } + + /** + * Creates the parameters for a SQL query by id. + * + * @param id the entity id. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param name the name to be used for the id parameter. + * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. + * @since 2.4 + */ + SqlIdentifierParameterSource forQueryById(Object id, Class domainType, SqlIdentifier name) { + + SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(dialect.getIdentifierProcessing()); + + addConvertedPropertyValue( // + parameterSource, // + getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // + id, // + name // + ); + return parameterSource; + } + + /** + * Creates the parameters for a SQL query by ids. + * + * @param ids the entity ids. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. + * @since 2.4 + */ + SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainType) { + + SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(dialect.getIdentifierProcessing()); + + addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), + ids, IDS_SQL_PARAMETER); + + return parameterSource; + } + + /** + * Creates the parameters for a SQL query of related entities. + * + * @param identifier the identifier describing the relation. Must not be {@code null}. + * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. + * @since 2.4 + */ + SqlIdentifierParameterSource forQueryByIdentifier(Identifier identifier) { + + SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(dialect.getIdentifierProcessing()); + + identifier.toMap() + .forEach((name, value) -> addConvertedPropertyValue(parameterSource, name, value, value.getClass())); + + return parameterSource; + } + + /** + * Utility to create {@link Predicate}s. + */ + static class Predicates { + + /** + * Include all {@link Predicate} returning {@literal false} to never skip a property. + * + * @return the include all {@link Predicate}. + */ + static Predicate includeAll() { + return it -> false; + } + } + + private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, + RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { + + addConvertedValue(parameterSource, value, name, converter.getColumnType(property), converter.getTargetSqlType(property)); + } + + private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, SqlIdentifier name, Object value, + Class javaType) { + + addConvertedValue(parameterSource, value, name, javaType, JdbcUtil.targetSqlTypeFor(javaType)); + } + + private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, + SqlIdentifier paramName, Class javaType, SQLType sqlType) { + + JdbcValue jdbcValue = converter.writeJdbcValue( // + value, // + javaType, // + sqlType // + ); + + parameterSource.addValue( // + paramName, // + jdbcValue.getValue(), // + jdbcValue.getJdbcType().getVendorTypeNumber()); + } + + private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, + RelationalPersistentProperty property, Iterable values, SqlIdentifier paramName) { + + List convertedIds = new ArrayList<>(); + JdbcValue jdbcValue = null; + for (Object id : values) { + + Class columnType = converter.getColumnType(property); + SQLType sqlType = converter.getTargetSqlType(property); + + jdbcValue = converter.writeJdbcValue(id, columnType, sqlType); + convertedIds.add(jdbcValue.getValue()); + } + + Assert.state(jdbcValue != null, "JdbcValue must be not null at this point. Please report this as a bug."); + + SQLType jdbcType = jdbcValue.getJdbcType(); + int typeNumber = jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); + + parameterSource.addValue(paramName, convertedIds, typeNumber); + } + + @SuppressWarnings("unchecked") + private RelationalPersistentEntity getRequiredPersistentEntity(Class domainType) { + return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); + } + + private SqlIdentifierParameterSource getParameterSource(@Nullable S instance, + RelationalPersistentEntity persistentEntity, String prefix, + Predicate skipProperty, IdentifierProcessing identifierProcessing) { + + SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(identifierProcessing); + + PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) + : NoValuePropertyAccessor.instance(); + + persistentEntity.doWithAll(property -> { + + if (skipProperty.test(property) || !property.isWritable()) { + return; + } + if (property.isEntity() && !property.isEmbedded()) { + return; + } + + if (property.isEmbedded()) { + + Object value = propertyAccessor.getProperty(property); + RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); + SqlIdentifierParameterSource additionalParameters = getParameterSource((T) value, + (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty, + identifierProcessing); + parameters.addAll(additionalParameters); + } else { + + Object value = propertyAccessor.getProperty(property); + SqlIdentifier paramName = property.getColumnName().transform(prefix::concat); + + addConvertedPropertyValue(parameters, property, value, paramName); + } + }); + + return parameters; + } + + /** + * A {@link PersistentPropertyAccessor} implementation always returning null + * + * @param + */ + static class NoValuePropertyAccessor implements PersistentPropertyAccessor { + + private static final NoValuePropertyAccessor INSTANCE = new NoValuePropertyAccessor(); + + static NoValuePropertyAccessor instance() { + return INSTANCE; + } + + @Override + public void setProperty(PersistentProperty property, @Nullable Object value) { + throw new UnsupportedOperationException("Cannot set value on 'null' target object."); + } + + @Override + public Object getProperty(PersistentProperty property) { + return null; + } + + @Override + public T getBean() { + return null; + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 0a5569d830..287f1cc02e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -29,15 +30,10 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.convert.CascadingDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.Identifier; -import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.*; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -63,6 +59,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor */ public class MyBatisDataAccessStrategy implements DataAccessStrategy { @@ -103,11 +100,15 @@ public static DataAccessStrategy createCombinedAccessStrategy(RelationalMappingC asList(myBatisDataAccessStrategy, delegatingDataAccessStrategy)); SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(context, converter, dialect); + SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter, dialect); + InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(operations, new BatchJdbcOperations(operations.getJdbcOperations()), dialect); DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy( // sqlGeneratorSource, // context, // converter, // - operations // + operations, // + sqlParametersFactory, // + insertStrategyFactory // ); delegatingDataAccessStrategy.setDelegate(defaultDataAccessStrategy); @@ -146,12 +147,21 @@ public void setNamespaceStrategy(NamespaceStrategy namespaceStrategy) { this.namespaceStrategy = namespaceStrategy; } + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + + MyBatisContext myBatisContext = new MyBatisContext(identifier, instance, domainType); + sqlSession().insert(namespace(domainType) + ".insert", myBatisContext); + + return myBatisContext.getId(); + } + /* * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, ParentKeys) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, Identifier, boolean) */ @Override - public Object insert(T instance, Class domainType, Identifier identifier) { + public Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { MyBatisContext myBatisContext = new MyBatisContext(identifier, instance, domainType); sqlSession().insert(namespace(domainType) + ".insert", myBatisContext); @@ -159,6 +169,14 @@ public Object insert(T instance, Class domainType, Identifier identifier) return myBatisContext.getId(); } + @Override + public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { + + return insertSubjects.stream() + .map(insertSubject -> insert(insertSubject.getInstance(), domainType, insertSubject.getIdentifier(), idValueSource)) + .toArray(); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index 6a7702edc9..d905241e37 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 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. @@ -34,14 +34,7 @@ import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; -import org.springframework.data.jdbc.core.convert.DataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; -import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; -import org.springframework.data.jdbc.core.convert.RelationResolver; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.*; import org.springframework.data.jdbc.core.dialect.JdbcArrayColumns; import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -61,6 +54,7 @@ * @author Michael Simons * @author Christoph Strobl * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 1.1 */ @Configuration(proxyBeanMethods = false) @@ -176,7 +170,8 @@ public JdbcAggregateTemplate jdbcAggregateTemplate(ApplicationContext applicatio public DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations operations, JdbcConverter jdbcConverter, JdbcMappingContext context, Dialect dialect) { return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, jdbcConverter, dialect), context, - jdbcConverter, operations); + jdbcConverter, operations, new SqlParametersFactory(context, jdbcConverter, dialect), + new InsertStrategyFactory(operations, new BatchJdbcOperations(operations.getJdbcOperations()), dialect)); } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index f52efa84e6..275e3defd5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -21,10 +21,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.data.jdbc.core.convert.BatchJdbcOperations; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.convert.InsertStrategyFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.SqlParametersFactory; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.dialect.Dialect; @@ -45,6 +48,7 @@ * @author Oliver Gierke * @author Mark Paluch * @author Hebert Coelho + * @author Chirag Tailor */ public class JdbcRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport implements ApplicationEventPublisherAware { @@ -185,8 +189,12 @@ public void afterPropertiesSet() { SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(this.mappingContext, this.converter, this.dialect); + SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(this.mappingContext, this.converter, + this.dialect); + InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(this.operations, + new BatchJdbcOperations(this.operations.getJdbcOperations()), this.dialect); return new DefaultDataAccessStrategy(sqlGeneratorSource, this.mappingContext, this.converter, - this.operations); + this.operations, sqlParametersFactory, insertStrategyFactory); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java index d845ecc59b..f95843d012 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2022 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. @@ -40,6 +40,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.MutableAggregateChange; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; @@ -53,6 +54,7 @@ * * @author Jens Schauder * @author Myeonghyeon-Lee + * @author Chirag Tailor */ @Disabled public class AggregateChangeIdGenerationImmutableUnitTests { @@ -68,7 +70,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests { RelationalMappingContext context = new RelationalMappingContext(); JdbcConverter converter = mock(JdbcConverter.class); - DbAction.WithEntity rootInsert = new DbAction.InsertRoot<>(entity); + DbAction.WithEntity rootInsert = new DbAction.InsertRoot<>(entity, IdValueSource.GENERATED); private DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); @@ -393,7 +395,7 @@ DbAction.Insert createInsert(String propertyName, Object value, @Nullable Obj DbAction.Insert insert = new DbAction.Insert<>(value, context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert, - singletonMap(toPath(propertyName), key)); + singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); return insert; } @@ -404,7 +406,7 @@ DbAction.Insert createDeepInsert(String propertyName, Object value, Object ke PersistentPropertyPath propertyPath = toPath( parentInsert.getPropertyPath().toDotPath() + "." + propertyName); DbAction.Insert insert = new DbAction.Insert<>(value, propertyPath, parentInsert, - singletonMap(propertyPath, key)); + singletonMap(propertyPath, key), IdValueSource.GENERATED); return insert; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java index 1d7086e3e3..ad520ff985 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2022 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. @@ -37,6 +37,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.MutableAggregateChange; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -47,6 +48,7 @@ * * @author Jens Schauder * @author Myeonghyeon-Lee + * @author Chirag Tailor */ public class AggregateChangeIdGenerationUnitTests { @@ -61,7 +63,7 @@ public class AggregateChangeIdGenerationUnitTests { JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); - DbAction.WithEntity rootInsert = new DbAction.InsertRoot<>(entity); + DbAction.WithEntity rootInsert = new DbAction.InsertRoot<>(entity, IdValueSource.GENERATED); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class, new IncrementingIds()); AggregateChangeExecutor executor = new AggregateChangeExecutor(converter, accessStrategy); @@ -325,7 +327,7 @@ public void setIdForDeepElementMapElementMap() { DbAction.Insert createInsert(String propertyName, Object value, @Nullable Object key) { return new DbAction.Insert<>(value, context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert, - key == null ? emptyMap() : singletonMap(toPath(propertyName), key)); + key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); } DbAction.Insert createDeepInsert(String propertyName, Object value, @Nullable Object key, @@ -335,7 +337,7 @@ DbAction.Insert createDeepInsert(String propertyName, Object value, @Nullable parentInsert.getPropertyPath().toDotPath() + "." + propertyName); return new DbAction.Insert<>(value, propertyPath, parentInsert, - key == null ? emptyMap() : singletonMap(propertyPath, key)); + key == null ? emptyMap() : singletonMap(propertyPath, key), IdValueSource.GENERATED); } PersistentPropertyPath toPath(String path) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 6e532c1faf..ee52283881 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -36,6 +36,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -66,9 +67,9 @@ public void rootOfEmptySetOfActionsisNull() { public void afterInsertRootIdAndVersionMaybeUpdated() { // note that the root entity isn't the original one, but a new instance with the version set. - when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()))).thenReturn(23L); + when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()), eq(IdValueSource.GENERATED))).thenReturn(23L); - executionContext.executeInsertRoot(new DbAction.InsertRoot<>(root)); + executionContext.executeInsertRoot(new DbAction.InsertRoot<>(root, IdValueSource.GENERATED)); DummyEntity newRoot = executionContext.populateIdsIfNecessary(); @@ -85,10 +86,10 @@ public void idGenerationOfChild() { Content content = new Content(); - when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()))).thenReturn(23L); - when(accessStrategy.insert(any(Content.class), eq(Content.class), eq(createBackRef()))).thenReturn(24L); + when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()), eq(IdValueSource.GENERATED))).thenReturn(23L); + when(accessStrategy.insert(any(Content.class), eq(Content.class), eq(createBackRef()), eq(IdValueSource.GENERATED))).thenReturn(24L); - DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "content", content, null)); @@ -105,10 +106,10 @@ public void idGenerationOfChildInList() { Content content = new Content(); - when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()))).thenReturn(23L); - when(accessStrategy.insert(eq(content), eq(Content.class), any(Identifier.class))).thenReturn(24L); + when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()), eq(IdValueSource.GENERATED))).thenReturn(23L); + when(accessStrategy.insert(eq(content), eq(Content.class), any(Identifier.class), eq(IdValueSource.GENERATED))).thenReturn(24L); - DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "list", content, 1)); @@ -124,7 +125,7 @@ DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyNa @Nullable Object key) { DbAction.Insert insert = new DbAction.Insert<>(value, getPersistentPropertyPath(propertyName), parent, - key == null ? emptyMap() : singletonMap(toPath(propertyName), key)); + key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); return insert; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java index afc4990ff9..f00a025697 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 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. @@ -30,12 +30,15 @@ import org.springframework.data.jdbc.core.convert.Identifier; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; +import org.springframework.data.jdbc.core.convert.InsertSubject; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; /** @@ -43,6 +46,7 @@ * * @author Jens Schauder * @author Umut Erturk + * @author Chirag Tailor */ public class JdbcAggregateChangeExecutorContextUnitTests { @@ -69,9 +73,9 @@ public void rootOfEmptySetOfActionsisNull() { @Test // DATAJDBC-453 public void afterInsertRootIdAndVersionMaybeUpdated() { - when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty())).thenReturn(23L); + when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(23L); - executionContext.executeInsertRoot(new DbAction.InsertRoot<>(root)); + executionContext.executeInsertRoot(new DbAction.InsertRoot<>(root, IdValueSource.GENERATED)); DummyEntity newRoot = executionContext.populateIdsIfNecessary(); @@ -88,7 +92,7 @@ public void afterInsertNotPrimitiveVersionShouldBeZero() { DummyEntityNonPrimitiveVersion dummyEntityNonPrimitiveVersion = new DummyEntityNonPrimitiveVersion(); - executionContext.executeInsertRoot(new DbAction.InsertRoot<>(dummyEntityNonPrimitiveVersion)); + executionContext.executeInsertRoot(new DbAction.InsertRoot<>(dummyEntityNonPrimitiveVersion, IdValueSource.GENERATED)); executionContext.populateRootVersionIfNecessary(dummyEntityNonPrimitiveVersion); assertThat(dummyEntityNonPrimitiveVersion.version).isEqualTo(0); @@ -99,10 +103,10 @@ public void idGenerationOfChild() { Content content = new Content(); - when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty())).thenReturn(23L); - when(accessStrategy.insert(content, Content.class, createBackRef())).thenReturn(24L); + when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(23L); + when(accessStrategy.insert(content, Content.class, createBackRef(), IdValueSource.GENERATED)).thenReturn(24L); - DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "content", content, null)); @@ -119,10 +123,10 @@ public void idGenerationOfChildInList() { Content content = new Content(); - when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty())).thenReturn(23L); - when(accessStrategy.insert(eq(content), eq(Content.class), any(Identifier.class))).thenReturn(24L); + when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(23L); + when(accessStrategy.insert(eq(content), eq(Content.class), any(Identifier.class), eq(IdValueSource.GENERATED))).thenReturn(24L); - DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "list", content, 1)); @@ -134,11 +138,65 @@ public void idGenerationOfChildInList() { assertThat(content.id).isEqualTo(24L); } + @Test // GH-1159 + void batchInsertOperation_withGeneratedIds() { + + when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(123L); + + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); + executionContext.executeInsertRoot(rootInsert); + + Content content = new Content(); + Identifier identifier = Identifier.empty() + .withPart(SqlIdentifier.quoted("DUMMY_ENTITY"), 123L, Long.class) + .withPart(SqlIdentifier.quoted("DUMMY_ENTITY_KEY"), 0, Integer.class); + when(accessStrategy.insert(singletonList(InsertSubject.describedBy(content, identifier)), Content.class, IdValueSource.GENERATED)) + .thenReturn(new Object[] { 456L }); + DbAction.InsertBatch insertBatch = new DbAction.InsertBatch<>( + singletonList(createInsert(rootInsert, "list", content, 0)), + IdValueSource.GENERATED + ); + executionContext.executeInsertBatch(insertBatch); + + DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + + assertThat(newRoot).isNull(); + assertThat(root.id).isEqualTo(123L); + assertThat(content.id).isEqualTo(456L); + } + + @Test // GH-1159 + void batchInsertOperation_withoutGeneratedIds() { + + when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(123L); + + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); + executionContext.executeInsertRoot(rootInsert); + + Content content = new Content(); + Identifier identifier = Identifier.empty() + .withPart(SqlIdentifier.quoted("DUMMY_ENTITY"), 123L, Long.class) + .withPart(SqlIdentifier.quoted("DUMMY_ENTITY_KEY"), 0, Integer.class); + when(accessStrategy.insert(singletonList(InsertSubject.describedBy(content, identifier)), Content.class, IdValueSource.PROVIDED)) + .thenReturn(new Object[] { null }); + DbAction.InsertBatch insertBatch = new DbAction.InsertBatch<>( + singletonList(createInsert(rootInsert, "list", content, 0)), + IdValueSource.PROVIDED + ); + executionContext.executeInsertBatch(insertBatch); + + DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + + assertThat(newRoot).isNull(); + assertThat(root.id).isEqualTo(123L); + assertThat(content.id).isNull(); + } + DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyName, Object value, - @Nullable Object key) { + @Nullable Object key) { DbAction.Insert insert = new DbAction.Insert<>(value, getPersistentPropertyPath(propertyName), parent, - key == null ? emptyMap() : singletonMap(toPath(propertyName), key)); + key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); return insert; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java index 5098823c29..2f2cf214f2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -15,41 +15,23 @@ */ package org.springframework.data.jdbc.core.convert; -import static java.util.Arrays.*; import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import static org.springframework.data.relational.core.sql.SqlIdentifier.*; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.Value; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; -import org.springframework.data.convert.ReadingConverter; -import org.springframework.data.convert.WritingConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.HsqlDbDialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.jdbc.support.KeyHolder; + +import lombok.RequiredArgsConstructor; /** * Unit tests for {@link DefaultDataAccessStrategy}. @@ -59,19 +41,17 @@ * @author Myeonghyeon Lee * @author Myat Min * @author Radim Tlusty + * @author Chirag Tailor */ public class DefaultDataAccessStrategyUnitTests { - public static final long ID_FROM_ADDITIONAL_VALUES = 23L; public static final long ORIGINAL_ID = 4711L; - public static final long GENERATED_ID = 17; NamedParameterJdbcOperations namedJdbcOperations = mock(NamedParameterJdbcOperations.class); JdbcOperations jdbcOperations = mock(JdbcOperations.class); RelationalMappingContext context = new JdbcMappingContext(); - - HashMap additionalParameters = new HashMap<>(); - ArgumentCaptor paramSourceCaptor = ArgumentCaptor.forClass(SqlParameterSource.class); + SqlParametersFactory sqlParametersFactory = mock(SqlParametersFactory.class); + InsertStrategyFactory insertStrategyFactory = mock(InsertStrategyFactory.class); JdbcConverter converter; DefaultDataAccessStrategy accessStrategy; @@ -81,164 +61,54 @@ public void before() { DelegatingDataAccessStrategy relationResolver = new DelegatingDataAccessStrategy(); Dialect dialect = HsqlDbDialect.INSTANCE; - converter = new BasicJdbcConverter(context, relationResolver, new JdbcCustomConversions(), new DefaultJdbcTypeFactory(jdbcOperations), dialect.getIdentifierProcessing()); accessStrategy = new DefaultDataAccessStrategy( // new SqlGeneratorSource(context, converter, dialect), // context, // converter, // - namedJdbcOperations); + namedJdbcOperations, // + sqlParametersFactory, // + insertStrategyFactory); relationResolver.setDelegate(accessStrategy); + + when(sqlParametersFactory.forInsert(any(), any(), any(), any())) + .thenReturn(new SqlIdentifierParameterSource(dialect.getIdentifierProcessing())); + when(insertStrategyFactory.insertStrategy(any(), any())).thenReturn(mock(InsertStrategy.class)); + when(insertStrategyFactory.batchInsertStrategy(any(), any())).thenReturn(mock(BatchInsertStrategy.class)); } - @Test // DATAJDBC-146 - public void additionalParameterForIdDoesNotLeadToDuplicateParameters() { - - additionalParameters.put(SqlIdentifier.quoted("ID"), ID_FROM_ADDITIONAL_VALUES); - - accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.from(additionalParameters)); - - verify(namedJdbcOperations).update(eq("INSERT INTO \"DUMMY_ENTITY\" (\"ID\") VALUES (:ID)"), - paramSourceCaptor.capture()); - } - - @Test // DATAJDBC-146 - public void additionalParametersGetAddedToStatement() { - - ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); - - additionalParameters.put(unquoted("reference"), ID_FROM_ADDITIONAL_VALUES); - - accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.from(additionalParameters)); - - verify(namedJdbcOperations).update(sqlCaptor.capture(), paramSourceCaptor.capture()); - - assertThat(sqlCaptor.getValue()) // - .containsSubsequence("INSERT INTO \"DUMMY_ENTITY\" (", "\"ID\"", ") VALUES (", ":id", ")") // - .containsSubsequence("INSERT INTO \"DUMMY_ENTITY\" (", "reference", ") VALUES (", ":reference", ")"); - assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(ORIGINAL_ID); - } - - @Test // DATAJDBC-235 - public void considersConfiguredWriteConverter() { - - DefaultDataAccessStrategy accessStrategy = createAccessStrategyWithConverter( - asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE)); + @Test // GH-1159 + public void insert() { - ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.empty(), IdValueSource.PROVIDED); - EntityWithBoolean entity = new EntityWithBoolean(ORIGINAL_ID, true); - - accessStrategy.insert(entity, EntityWithBoolean.class, Identifier.empty()); - - verify(namedJdbcOperations).update(sqlCaptor.capture(), paramSourceCaptor.capture()); - - assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(ORIGINAL_ID); - assertThat(paramSourceCaptor.getValue().getValue("flag")).isEqualTo("T"); + verify(insertStrategyFactory).insertStrategy(IdValueSource.PROVIDED, SqlIdentifier.quoted("ID")); } - @Test // DATAJDBC-412 - public void considersConfiguredWriteConverterForIdValueObjects() { - - DefaultDataAccessStrategy accessStrategy = createAccessStrategyWithConverter( - singletonList(IdValueToStringConverter.INSTANCE)); - - String rawId = "batman"; + @Test // GH-1159 + public void batchInsert() { - WithValueObjectId entity = new WithValueObjectId(new IdValue(rawId)); - entity.value = "vs. superman"; + accessStrategy.insert(singletonList(InsertSubject.describedBy(new DummyEntity(ORIGINAL_ID), Identifier.empty())), DummyEntity.class, IdValueSource.PROVIDED); - accessStrategy.insert(entity, WithValueObjectId.class, Identifier.empty()); - - verify(namedJdbcOperations).update(anyString(), paramSourceCaptor.capture()); - - assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(rawId); - assertThat(paramSourceCaptor.getValue().getValue("value")).isEqualTo("vs. superman"); - - accessStrategy.findById(new IdValue(rawId), WithValueObjectId.class); - - verify(namedJdbcOperations).queryForObject(anyString(), paramSourceCaptor.capture(), any(EntityRowMapper.class)); - assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(rawId); + verify(insertStrategyFactory).batchInsertStrategy(IdValueSource.PROVIDED, SqlIdentifier.quoted("ID")); } - @Test // DATAJDBC-349 - public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInOneToManyRelationship() { - - DefaultDataAccessStrategy accessStrategy = createAccessStrategyWithConverter( - singletonList(IdValueToStringConverter.INSTANCE)); + @Test // GH-1159 + public void insertForEntityWithNoId() { - String rawId = "batman"; - IdValue rootIdValue = new IdValue(rawId); + accessStrategy.insert(new DummyEntityWithoutIdAnnotation(ORIGINAL_ID), DummyEntityWithoutIdAnnotation.class, Identifier.empty(), IdValueSource.GENERATED); - DummyEntityRoot root = new DummyEntityRoot(rootIdValue); - DummyEntity child = new DummyEntity(ORIGINAL_ID); - root.dummyEntities.add(child); - - additionalParameters.put(SqlIdentifier.quoted("DUMMYENTITYROOT"), rootIdValue); - accessStrategy.insert(root, DummyEntityRoot.class, Identifier.from(additionalParameters)); - - verify(namedJdbcOperations).update(anyString(), paramSourceCaptor.capture()); - - assertThat(paramSourceCaptor.getValue().getValue("id")).isEqualTo(rawId); - - PersistentPropertyPath path = context.getPersistentPropertyPath("dummyEntities", - DummyEntityRoot.class); - - accessStrategy.findAllByPath(Identifier.from(additionalParameters), path); - - verify(namedJdbcOperations).query(anyString(), paramSourceCaptor.capture(), any(RowMapper.class)); - - assertThat(paramSourceCaptor.getValue().getValue("DUMMYENTITYROOT")).isEqualTo(rawId); + verify(insertStrategyFactory).insertStrategy(IdValueSource.GENERATED, null); } - @Test // GH-933 - public void insertWithDefinedIdDoesNotRetrieveGeneratedKeys() { - - Object generatedId = accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.from(additionalParameters)); + @Test // GH-1159 + public void batchInsertForEntityWithNoId() { - assertThat(generatedId).isNull(); + accessStrategy.insert(singletonList(InsertSubject.describedBy(new DummyEntityWithoutIdAnnotation(ORIGINAL_ID), Identifier.empty())), DummyEntityWithoutIdAnnotation.class, IdValueSource.GENERATED); - verify(namedJdbcOperations).update(eq("INSERT INTO \"DUMMY_ENTITY\" (\"ID\") VALUES (:id)"), - paramSourceCaptor.capture()); - } - - @Test // GH-933 - public void insertWithUndefinedIdRetrievesGeneratedKeys() { - - when(namedJdbcOperations.update(any(), any(), any())) - .then(invocation -> { - - KeyHolder keyHolder = invocation.getArgument(2); - keyHolder.getKeyList().add(singletonMap("ID", GENERATED_ID)); - return 1; - }); - - Object generatedId = accessStrategy.insert(new DummyEntity(null), DummyEntity.class, Identifier.from(additionalParameters)); - - assertThat(generatedId).isEqualTo(GENERATED_ID); - - verify(namedJdbcOperations).update(eq("INSERT INTO \"DUMMY_ENTITY\" VALUES (DEFAULT)"), - paramSourceCaptor.capture(), any(KeyHolder.class)); - } - - private DefaultDataAccessStrategy createAccessStrategyWithConverter(List converters) { - DelegatingDataAccessStrategy relationResolver = new DelegatingDataAccessStrategy(); - - Dialect dialect = HsqlDbDialect.INSTANCE; - - JdbcConverter converter = new BasicJdbcConverter(context, relationResolver, new JdbcCustomConversions(converters), - new DefaultJdbcTypeFactory(jdbcOperations), dialect.getIdentifierProcessing()); - - DefaultDataAccessStrategy accessStrategy = new DefaultDataAccessStrategy( // - new SqlGeneratorSource(context, converter, dialect), // - context, // - converter, // - namedJdbcOperations); - - relationResolver.setDelegate(accessStrategy); - return accessStrategy; + verify(insertStrategyFactory).batchInsertStrategy(IdValueSource.GENERATED, null); } @RequiredArgsConstructor @@ -247,62 +117,9 @@ private static class DummyEntity { @Id private final Long id; } - @RequiredArgsConstructor // DATAJDBC-349 - private static class DummyEntityRoot { - - @Id private final IdValue id; - List dummyEntities = new ArrayList<>(); - } - - @AllArgsConstructor - private static class EntityWithBoolean { - - @Id Long id; - boolean flag; - } - - @Data - private static class WithValueObjectId { - - @Id private final IdValue id; - String value; - } - - @Value - private static class IdValue { - String id; - } - - @WritingConverter - enum BooleanToStringConverter implements Converter { - - INSTANCE; - - @Override - public String convert(Boolean source) { - return source != null && source ? "T" : "F"; - } - } - - @ReadingConverter - enum StringToBooleanConverter implements Converter { - - INSTANCE; - - @Override - public Boolean convert(String source) { - return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE; - } - } - - @WritingConverter - enum IdValueToStringConverter implements Converter { - - INSTANCE; + @RequiredArgsConstructor + private static class DummyEntityWithoutIdAnnotation { - @Override - public String convert(IdValue source) { - return source.id; - } + private final Long id; } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategyTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategyTest.java new file mode 100644 index 0000000000..577528308e --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingBatchInsertStrategyTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; +import org.springframework.data.jdbc.core.JdbcAggregateOperations; +import org.springframework.data.relational.core.dialect.AbstractDialect; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.IdGeneration; +import org.springframework.data.relational.core.dialect.LimitClause; +import org.springframework.data.relational.core.dialect.LockClause; +import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * Unit tests for {@link IdGeneratingBatchInsertStrategy}. + * + * @author Chirag Tailor + */ +class IdGeneratingBatchInsertStrategyTest { + + SqlIdentifier idColumn = SqlIdentifier.quoted("id"); + IdentifierProcessing identifierProcessing = IdentifierProcessing.ANSI; + BatchJdbcOperations batchJdbcOperations = mock(BatchJdbcOperations.class); + InsertStrategy insertStrategy = mock(InsertStrategy.class); + String sql = "some sql"; + SqlParameterSource[] sqlParameterSources = new SqlParameterSource[]{new SqlIdentifierParameterSource(identifierProcessing) }; + + @Test + void insertsSequentially_whenIdGenerationForBatchOperationsNotSupported() { + + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, true, false), + batchJdbcOperations, idColumn); + + SqlIdentifierParameterSource sqlParameterSource1 = new SqlIdentifierParameterSource(identifierProcessing); + sqlParameterSource1.addValue(SqlIdentifier.quoted("property1"), "value1"); + SqlIdentifierParameterSource sqlParameterSource2 = new SqlIdentifierParameterSource(identifierProcessing); + sqlParameterSource2.addValue(SqlIdentifier.quoted("property2"), "value2"); + + long id1 = 1L; + when(insertStrategy.execute(sql, sqlParameterSource1)).thenReturn(id1); + long id2 = 2L; + when(insertStrategy.execute(sql, sqlParameterSource2)).thenReturn(id2); + + Object[] ids = batchInsertStrategy.execute(sql, new SqlParameterSource[]{sqlParameterSource1, sqlParameterSource2}); + + assertThat(ids).containsExactly(id1, id2); + } + + @Test + void insertsWithKeyHolderAndKeyColumnNames_whenDriverRequiresKeyColumnNames() { + + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, true, true), + batchJdbcOperations, idColumn); + + batchInsertStrategy.execute(sql, sqlParameterSources); + + verify(batchJdbcOperations).batchUpdate(eq(sql), eq(sqlParameterSources), any(KeyHolder.class), + eq(new String[] { idColumn.getReference() })); + } + + @Test + void insertsWithKeyHolder_whenDriverRequiresKeyColumnNames_butIdColumnIsNull() { + + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, true, true), + batchJdbcOperations, null); + + batchInsertStrategy.execute(sql, sqlParameterSources); + + verify(batchJdbcOperations).batchUpdate(eq(sql), eq(sqlParameterSources), any(KeyHolder.class)); + } + + @Test + void insertsWithKeyHolder_whenDriverDoesNotRequireKeyColumnNames() { + + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, false, true), + batchJdbcOperations, idColumn); + + batchInsertStrategy.execute(sql, sqlParameterSources); + + verify(batchJdbcOperations).batchUpdate(eq(sql), eq(sqlParameterSources), any(KeyHolder.class)); + } + + @Test + void insertsWithKeyHolder_returningKey_whenThereIsOnlyOne() { + + Long idValue = 123L; + when(batchJdbcOperations.batchUpdate(any(), any(), any())).thenAnswer(invocationOnMock -> { + KeyHolder keyHolder = invocationOnMock.getArgument(2); + HashMap keys = new HashMap<>(); + keys.put("anything", idValue); + keyHolder.getKeyList().add(keys); + return null; + }); + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, false, true), + batchJdbcOperations, idColumn); + + Object[] ids = batchInsertStrategy.execute(sql, sqlParameterSources); + + assertThat(ids).containsExactly(idValue); + } + + @Test + void insertsWithKeyHolder_returningKeyMatchingIdColumn_whenKeyHolderContainsMultipleKeysPerRecord() { + + Long idValue = 123L; + when(batchJdbcOperations.batchUpdate(any(), any(), any())).thenAnswer(invocationOnMock -> { + KeyHolder keyHolder = invocationOnMock.getArgument(2); + HashMap keys = new HashMap<>(); + keys.put(idColumn.getReference(), idValue); + keys.put("other", "someOtherValue"); + keyHolder.getKeyList().add(keys); + return null; + }); + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, false, true), + batchJdbcOperations, idColumn); + + Object[] ids = batchInsertStrategy.execute(sql, sqlParameterSources); + + assertThat(ids).containsExactly(idValue); + } + + @Test + void insertsWithKeyHolder_returningNull__whenKeyHolderContainsMultipleKeysPerRecord_butIdColumnIsNull() { + + Long idValue = 123L; + when(batchJdbcOperations.batchUpdate(any(), any(), any())).thenAnswer(invocationOnMock -> { + KeyHolder keyHolder = invocationOnMock.getArgument(2); + HashMap keys = new HashMap<>(); + keys.put(idColumn.getReference(), idValue); + keys.put("other", "someOtherValue"); + keyHolder.getKeyList().add(keys); + return null; + }); + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, false, true), + batchJdbcOperations, null); + + Object[] ids = batchInsertStrategy.execute(sql, sqlParameterSources); + + assertThat(ids).hasSize(sqlParameterSources.length); + assertThat(ids).containsOnlyNulls(); + } + + @Test + void insertsWithKeyHolder_returningNull_whenKeyHolderHasNoKeys() { + + BatchInsertStrategy batchInsertStrategy = new IdGeneratingBatchInsertStrategy( + insertStrategy, + createDialect(identifierProcessing, false, true), + batchJdbcOperations, idColumn); + + Object[] ids = batchInsertStrategy.execute(sql, sqlParameterSources); + + assertThat(ids).hasSize(sqlParameterSources.length); + assertThat(ids).containsOnlyNulls(); + } + + private static Dialect createDialect(final IdentifierProcessing identifierProcessing, + final boolean requiresKeyColumnNames, + final boolean supportsIdGenerationForBatchOperations) { + + return new AbstractDialect() { + + @Override + public LimitClause limit() { + return null; + } + + @Override + public LockClause lock() { + return null; + } + + @Override + public IdentifierProcessing getIdentifierProcessing() { + return identifierProcessing; + } + + @Override + public IdGeneration getIdGeneration() { + return new IdGeneration() { + @Override + public boolean driverRequiresKeyColumnNames() { + return requiresKeyColumnNames; + } + + @Override + public boolean supportedForBatchOperations() { + return supportsIdGenerationForBatchOperations; + } + }; + } + }; + } +} \ No newline at end of file diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategyTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategyTest.java new file mode 100644 index 0000000000..0e63b4f1b6 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/IdGeneratingInsertStrategyTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.dialect.AbstractDialect; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.IdGeneration; +import org.springframework.data.relational.core.dialect.LimitClause; +import org.springframework.data.relational.core.dialect.LockClause; +import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * Unit tests for {@link IdGeneratingInsertStrategy}. + * + * @author Chirag Tailor + */ +class IdGeneratingInsertStrategyTest { + + SqlIdentifier idColumn = SqlIdentifier.quoted("id"); + IdentifierProcessing identifierProcessing = IdentifierProcessing.ANSI; + NamedParameterJdbcOperations namedParameterJdbcOperations = mock(NamedParameterJdbcOperations.class); + String sql = "some sql"; + SqlParameterSource sqlParameterSource = new SqlIdentifierParameterSource(identifierProcessing); + + @Test + void insertsWithKeyHolderAndKeyColumnNames_whenDriverRequiresKeyColumnNames() { + + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, true), + namedParameterJdbcOperations, idColumn); + + insertStrategy.execute(sql, sqlParameterSource); + + verify(namedParameterJdbcOperations).update(eq(sql), eq(sqlParameterSource), any(KeyHolder.class), + eq(new String[] { idColumn.getReference() })); + } + + @Test + void insertsWithKeyHolder_whenDriverRequiresKeyColumnNames_butIdColumnIsNull() { + + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, true), + namedParameterJdbcOperations, null); + + insertStrategy.execute(sql, sqlParameterSource); + + verify(namedParameterJdbcOperations).update(eq(sql), eq(sqlParameterSource), any(KeyHolder.class)); + } + + @Test + void insertsWithKeyHolder_whenDriverDoesNotRequireKeyColumnNames() { + + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, false), + namedParameterJdbcOperations, idColumn); + + insertStrategy.execute(sql, sqlParameterSource); + + verify(namedParameterJdbcOperations).update(eq(sql), eq(sqlParameterSource), any(KeyHolder.class)); + } + + @Test + void insertsWithKeyHolder_returningKey_whenThereIsOnlyOne() { + + Long idValue = 123L; + when(namedParameterJdbcOperations.update(any(), any(), any())).thenAnswer(invocationOnMock -> { + KeyHolder keyHolder = invocationOnMock.getArgument(2); + HashMap keys = new HashMap<>(); + keys.put("anything", idValue); + keyHolder.getKeyList().add(keys); + return null; + }); + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, false), + namedParameterJdbcOperations, idColumn); + + Object id = insertStrategy.execute(sql, sqlParameterSource); + + assertThat(id).isEqualTo(idValue); + } + + @Test + void insertsWithKeyHolder_returningKeyMatchingIdColumn_whenKeyHolderContainsMultipleKeysPerRecord() { + + Long idValue = 123L; + when(namedParameterJdbcOperations.update(any(), any(), any())).thenAnswer(invocationOnMock -> { + KeyHolder keyHolder = invocationOnMock.getArgument(2); + HashMap keys = new HashMap<>(); + keys.put(idColumn.getReference(), idValue); + keys.put("other", "someOtherValue"); + keyHolder.getKeyList().add(keys); + return null; + }); + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, false), + namedParameterJdbcOperations, idColumn); + + Object id = insertStrategy.execute(sql, sqlParameterSource); + + assertThat(id).isEqualTo(idValue); + } + + @Test + void insertsWithKeyHolder_returningNull__whenKeyHolderContainsMultipleKeysPerRecord_butIdColumnIsNull() { + + Long idValue = 123L; + when(namedParameterJdbcOperations.update(any(), any(), any())).thenAnswer(invocationOnMock -> { + KeyHolder keyHolder = invocationOnMock.getArgument(2); + HashMap keys = new HashMap<>(); + keys.put(idColumn.getReference(), idValue); + keys.put("other", "someOtherValue"); + keyHolder.getKeyList().add(keys); + return null; + }); + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, false), + namedParameterJdbcOperations, null); + + Object id = insertStrategy.execute(sql, sqlParameterSource); + + assertThat(id).isNull(); + } + + @Test + void insertsWithKeyHolder_returningNull_whenKeyHolderHasNoKeys() { + + InsertStrategy insertStrategy = new IdGeneratingInsertStrategy(createDialect(identifierProcessing, false), + namedParameterJdbcOperations, idColumn); + + Object id = insertStrategy.execute(sql, sqlParameterSource); + + assertThat(id).isNull(); + } + + private static Dialect createDialect(final IdentifierProcessing identifierProcessing, + final boolean requiresKeyColumnNames) { + + return new AbstractDialect() { + + @Override + public LimitClause limit() { + return null; + } + + @Override + public LockClause lock() { + return null; + } + + @Override + public IdentifierProcessing getIdentifierProcessing() { + return identifierProcessing; + } + + @Override + public IdGeneration getIdGeneration() { + return new IdGeneration() { + @Override + public boolean driverRequiresKeyColumnNames() { + return requiresKeyColumnNames; + } + }; + } + }; + } +} \ No newline at end of file diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactoryTest.java new file mode 100644 index 0000000000..6a6dcf9794 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/InsertStrategyFactoryTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.dialect.AnsiDialect; +import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * Unit tests for {@link InsertStrategyFactory}. + * + * @author Chirag Tailor + */ +class InsertStrategyFactoryTest { + + IdentifierProcessing identifierProcessing = IdentifierProcessing.ANSI; + + NamedParameterJdbcOperations namedParameterJdbcOperations = mock(NamedParameterJdbcOperations.class); + BatchJdbcOperations batchJdbcOperations = mock(BatchJdbcOperations.class); + InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(namedParameterJdbcOperations, + batchJdbcOperations, AnsiDialect.INSTANCE); + + String sql = "some sql"; + SqlParameterSource sqlParameterSource = new SqlIdentifierParameterSource(identifierProcessing); + SqlParameterSource[] sqlParameterSources = new SqlParameterSource[] { sqlParameterSource }; + + @Test + void insertWithoutGeneratedIds() { + + Object id = insertStrategyFactory.insertStrategy(IdValueSource.GENERATED, null).execute(sql, sqlParameterSource); + + verify(namedParameterJdbcOperations).update(sql, sqlParameterSource); + assertThat(id).isNull(); + } + + @Test + void batchInsertWithoutGeneratedIds() { + + Object[] ids = insertStrategyFactory.batchInsertStrategy(IdValueSource.GENERATED, null).execute(sql, sqlParameterSources); + + verify(namedParameterJdbcOperations).batchUpdate(sql, sqlParameterSources); + assertThat(ids).hasSize(sqlParameterSources.length); + assertThat(ids).containsOnlyNulls(); + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java new file mode 100644 index 0000000000..e7a5b56e6c --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java @@ -0,0 +1,220 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.jdbc.core.convert; + +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategyUnitTests.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.dialect.AnsiDialect; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.jdbc.core.JdbcOperations; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +/** + * Unit tests for {@link SqlParametersFactory}. + * + * @author Chirag Tailor + */ +class SqlParametersFactoryTest { + + RelationalMappingContext context = new JdbcMappingContext(); + RelationResolver relationResolver = mock(RelationResolver.class); + BasicJdbcConverter converter = new BasicJdbcConverter(context, relationResolver); + AnsiDialect dialect = AnsiDialect.INSTANCE; + SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter, dialect); + + @Test // DATAJDBC-412 + public void considersConfiguredWriteConverterForIdValueObjects_onRead() { + + SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( + singletonList(IdValueToStringConverter.INSTANCE)); + + String rawId = "batman"; + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forQueryById(new IdValue(rawId), WithValueObjectId.class, SqlGenerator.ID_SQL_PARAMETER); + + assertThat(sqlParameterSource.getValue("id")).isEqualTo(rawId); + } + + @Test // DATAJDBC-349 + public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInOneToManyRelationship() { + + SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( + singletonList(IdValueToStringConverter.INSTANCE)); + + String rawId = "batman"; + IdValue rootIdValue = new IdValue(rawId); + + DummyEntityRoot root = new DummyEntityRoot(rootIdValue); + DummyEntity child = new DummyEntity(ORIGINAL_ID); + root.dummyEntities.add(child); + + HashMap additionalParameters = new HashMap<>(); + additionalParameters.put(SqlIdentifier.quoted("DUMMYENTITYROOT"), rootIdValue); + + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forQueryByIdentifier(Identifier.from(additionalParameters)); + + assertThat(sqlParameterSource.getValue("DUMMYENTITYROOT")).isEqualTo(rawId); + } + + @Test // DATAJDBC-146 + void identifiersGetAddedAsParameters() { + + long id = 4711L; + DummyEntity instance = new DummyEntity(id); + long reference = 23L; + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(instance, DummyEntity.class, + Identifier.of(SqlIdentifier.unquoted("reference"), reference, Long.class), IdValueSource.PROVIDED); + + assertThat(sqlParameterSource.getParameterNames()).hasSize(2); + assertThat(sqlParameterSource.getValue("id")).isEqualTo(id); + assertThat(sqlParameterSource.getValue("reference")).isEqualTo(reference); + } + + @Test // DATAJDBC-146 + void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { + + long id = 4711L; + DummyEntity instance = new DummyEntity(id); + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(instance, DummyEntity.class, + Identifier.of(SqlIdentifier.unquoted("id"), 23L, Long.class), IdValueSource.PROVIDED); + + assertThat(sqlParameterSource.getParameterNames()).hasSize(1); + assertThat(sqlParameterSource.getValue("id")).isEqualTo(id); + } + + @Test // DATAJDBC-235 + void considersConfiguredWriteConverter() { + + SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( + asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE)); + + long id = 4711L; + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(new EntityWithBoolean(id, true), + EntityWithBoolean.class, Identifier.empty(), IdValueSource.PROVIDED); + + assertThat(sqlParameterSource.getValue("id")).isEqualTo(id); + assertThat(sqlParameterSource.getValue("flag")).isEqualTo("T"); + } + + @Test // DATAJDBC-412 + void considersConfiguredWriteConverterForIdValueObjects_onWrite() { + + SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( + singletonList(IdValueToStringConverter.INSTANCE)); + + String rawId = "batman"; + WithValueObjectId entity = new WithValueObjectId(new IdValue(rawId)); + String value = "vs. superman"; + entity.value = value; + + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, WithValueObjectId.class, + Identifier.empty(), IdValueSource.PROVIDED); + assertThat(sqlParameterSource.getValue("id")).isEqualTo(rawId); + assertThat(sqlParameterSource.getValue("value")).isEqualTo(value); + } + + @WritingConverter + enum IdValueToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(IdValue source) { + return source.id; + } + } + + @Data + private static class WithValueObjectId { + + @Id private final IdValue id; + String value; + } + + @Value + private static class IdValue { + String id; + } + + @WritingConverter + enum BooleanToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Boolean source) { + return source != null && source ? "T" : "F"; + } + } + + @ReadingConverter + enum StringToBooleanConverter implements Converter { + + INSTANCE; + + @Override + public Boolean convert(String source) { + return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE; + } + } + + @AllArgsConstructor + private static class EntityWithBoolean { + + @Id Long id; + boolean flag; + } + + @RequiredArgsConstructor // DATAJDBC-349 + private static class DummyEntityRoot { + + @Id private final IdValue id; + List dummyEntities = new ArrayList<>(); + } + + @RequiredArgsConstructor + private static class DummyEntity { + + @Id + private final Long id; + } + + private SqlParametersFactory createSqlParametersFactoryWithConverters(List converters) { + + BasicJdbcConverter converter = new BasicJdbcConverter(context, relationResolver, new JdbcCustomConversions(converters), + new DefaultJdbcTypeFactory(mock(JdbcOperations.class)), dialect.getIdentifierProcessing()); + return new SqlParametersFactory(context, converter, dialect); + } +} \ No newline at end of file diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategyUnitTests.java index 4e0a58b6fb..633774824f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 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. @@ -34,6 +34,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.IdentifierProcessing; @@ -44,6 +45,7 @@ * @author Jens Schauder * @author Mark Paluch * @author Tyler Van Gorder + * @author Chirag Tailor */ public class MyBatisDataAccessStrategyUnitTests { @@ -67,7 +69,7 @@ public void before() { @Test // DATAJDBC-123 public void insert() { - accessStrategy.insert("x", String.class, Identifier.from(singletonMap(unquoted("key"), "value"))); + accessStrategy.insert("x", String.class, Identifier.from(singletonMap(unquoted("key"), "value")), IdValueSource.GENERATED); verify(session).insert(eq("java.lang.StringMapper.insert"), captor.capture()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java index ba1af86b97..a31768b042 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -46,7 +46,7 @@ import org.springframework.data.jdbc.testing.EnabledOnFeature; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.relational.core.mapping.MappedCollection; -import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.relational.core.mapping.event.BeforeConvertEvent; import org.springframework.data.repository.CrudRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -60,6 +60,7 @@ * @author Jens Schauder * @author Thomas Lang * @author Yunyung LEE + * @author Chirag Tailor */ @ContextConfiguration @Transactional @@ -182,7 +183,7 @@ DummyEntityRepository dummyEntityRepository() { @Bean ApplicationListener applicationListener() { - return (ApplicationListener) beforeInsert -> ((EntityWithColumnsRequiringConversions) beforeInsert + return (ApplicationListener) event -> ((EntityWithColumnsRequiringConversions) event .getEntity()).setIdTimestamp(getNow()); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsAndManuallyAssignedIdHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsAndManuallyAssignedIdHsqlIntegrationTests.java index 70aa42f175..59d12cb48d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsAndManuallyAssignedIdHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsAndManuallyAssignedIdHsqlIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -36,7 +36,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.testing.TestConfiguration; -import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.relational.core.mapping.event.BeforeConvertEvent; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.context.ActiveProfiles; @@ -48,6 +48,7 @@ * Very simple use cases for creation and usage of JdbcRepositories. * * @author Jens Schauder + * @author Chirag Tailor */ @ContextConfiguration @Transactional @@ -76,7 +77,7 @@ DummyEntityRepository dummyEntityRepository() { @Bean public ApplicationListener idSetting() { - return (ApplicationListener) event -> { + return (ApplicationListener) event -> { if (event.getEntity() instanceof DummyEntity) { setIds((DummyEntity) event.getEntity()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithListsIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithListsIntegrationTests.java index 3235992683..5cb5d448fa 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithListsIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithListsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.repository; +import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature.*; import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; @@ -27,6 +28,7 @@ import java.util.HashMap; import java.util.List; +import lombok.Value; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -50,6 +52,7 @@ * * @author Jens Schauder * @author Thomas Lang + * @author Chirag Tailor */ @ContextConfiguration @Transactional @@ -59,6 +62,7 @@ public class JdbcRepositoryWithListsIntegrationTests { @Autowired NamedParameterJdbcTemplate template; @Autowired DummyEntityRepository repository; + @Autowired RootRepository rootRepository; private static DummyEntity createDummyEntity() { @@ -104,6 +108,30 @@ public void saveAndLoadNonEmptyList() { .containsExactlyInAnyOrder(element1.id, element2.id); } + @Test // GH-1159 + void saveAndLoadNonEmptyNestedList() { + Root root = new Root(); + Intermediate intermediate1 = new Intermediate(); + root.intermediates.add(intermediate1); + Intermediate intermediate2 = new Intermediate(); + root.intermediates.add(intermediate2); + Leaf leaf1 = new Leaf("leaf1"); + Leaf leaf2 = new Leaf("leaf2"); + intermediate1.leaves.addAll(asList(leaf1, leaf2)); + Leaf leaf3 = new Leaf("leaf3"); + Leaf leaf4 = new Leaf("leaf4"); + intermediate2.leaves.addAll(asList(leaf3, leaf4)); + + rootRepository.save(root); + + assertThat(root.id).isNotNull(); + assertThat(root.intermediates).allMatch(v -> v.id != null); + + Root reloaded = rootRepository.findById(root.id).orElseThrow(AssertionFailedError::new); + assertThat(reloaded.intermediates.get(0).leaves).containsExactly(leaf1, leaf2); + assertThat(reloaded.intermediates.get(1).leaves).containsExactly(leaf3, leaf4); + } + @Test // DATAJDBC-130 public void findAllLoadsList() { @@ -195,6 +223,8 @@ private Element createElement(String content) { interface DummyEntityRepository extends CrudRepository {} + interface RootRepository extends CrudRepository {} + @Configuration @Import(TestConfiguration.class) static class Config { @@ -210,6 +240,11 @@ Class testClass() { DummyEntityRepository dummyEntityRepository() { return factory.getRepository(DummyEntityRepository.class); } + + @Bean + RootRepository rootRepository() { + return factory.getRepository(RootRepository.class); + } } @Data @@ -228,4 +263,20 @@ static class Element { @Id private Long id; } + @Data + static class Root { + @Id private Long id; + List intermediates = new ArrayList<>(); + } + + @Data + static class Intermediate { + @Id private Long id; + List leaves = new ArrayList<>(); + } + + @Value + static class Leaf { + String name; + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index 31d51e8663..96a9380107 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -37,13 +37,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; -import org.springframework.data.jdbc.core.convert.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.*; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; @@ -52,6 +46,7 @@ import org.springframework.data.relational.core.dialect.HsqlDbDialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.event.*; +import org.springframework.data.relational.core.mapping.event.Identifier; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -68,6 +63,7 @@ * @author Myeonghyeon Lee * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor */ public class SimpleJdbcRepositoryEventsUnitTests { @@ -86,8 +82,10 @@ public void before() { JdbcConverter converter = new BasicJdbcConverter(context, delegatingDataAccessStrategy, new JdbcCustomConversions(), new DefaultJdbcTypeFactory(operations.getJdbcOperations()), dialect.getIdentifierProcessing()); SqlGeneratorSource generatorSource = new SqlGeneratorSource(context, converter, dialect); + SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter, dialect); + InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(operations, new BatchJdbcOperations(operations.getJdbcOperations()), dialect); - this.dataAccessStrategy = spy(new DefaultDataAccessStrategy(generatorSource, context, converter, operations)); + this.dataAccessStrategy = spy(new DefaultDataAccessStrategy(generatorSource, context, converter, operations, sqlParametersFactory, insertStrategyFactory)); delegatingDataAccessStrategy.setDelegate(dataAccessStrategy); doReturn(true).when(dataAccessStrategy).update(any(), any()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 6cc7918060..a94b535b5c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -35,10 +35,13 @@ import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; +import org.springframework.data.jdbc.core.convert.BatchJdbcOperations; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.convert.InsertStrategyFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.SqlParametersFactory; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; @@ -60,6 +63,7 @@ * @author Greg Turnquist * @author Evgeni Dimitrov * @author Fei Dong + * @author Chirag Tailor */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = TestConfiguration.class) @@ -159,7 +163,8 @@ DataAccessStrategy defaultDataAccessStrategy( @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, converter, dialect), context, converter, - template); + template, new SqlParametersFactory(context, converter, dialect), + new InsertStrategyFactory(template, new BatchJdbcOperations(template.getJdbcOperations()), dialect)); } @Bean diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index c1804f217c..92a57af14c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -33,14 +33,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.data.convert.CustomConversions; -import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; -import org.springframework.data.jdbc.core.convert.DataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; -import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; -import org.springframework.data.jdbc.core.convert.RelationResolver; -import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; +import org.springframework.data.jdbc.core.convert.*; import org.springframework.data.jdbc.core.dialect.JdbcArrayColumns; import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -66,6 +59,7 @@ * @author Fei Dong * @author Myeonghyeon Lee * @author Christoph Strobl + * @author Chirag Tailor */ @Configuration @ComponentScan // To pick up configuration classes (per activated profile) @@ -103,7 +97,9 @@ DataAccessStrategy defaultDataAccessStrategy( JdbcConverter converter, Dialect dialect) { DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy( - new SqlGeneratorSource(context, converter, dialect), context, converter, template); + new SqlGeneratorSource(context, converter, dialect), context, converter, template, + new SqlParametersFactory(context, converter, dialect), + new InsertStrategyFactory(template, new BatchJdbcOperations(template.getJdbcOperations()), dialect)); return defaultDataAccessStrategy; } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-db2.sql index 5532bbff1b..2bcdedbd0c 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-db2.sql @@ -3,3 +3,24 @@ DROP TABLE dummy_entity; CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key BIGINT, dummy_entity BIGINT); + +DROP TABLE root; +DROP TABLE intermediate; +DROP TABLE leaf; + +CREATE TABLE root +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY +); +CREATE TABLE intermediate +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-h2.sql index 73abf96ce1..4b9a10ace5 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-h2.sql @@ -1,2 +1,19 @@ CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key BIGINT, dummy_entity BIGINT); + +CREATE TABLE root +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY +); +CREATE TABLE intermediate +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-hsql.sql index 73abf96ce1..7a9df99a5b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-hsql.sql @@ -1,2 +1,19 @@ CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key BIGINT, dummy_entity BIGINT); + +CREATE TABLE root +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY +); +CREATE TABLE intermediate +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mariadb.sql index 4cff749611..25c1e795ef 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mariadb.sql @@ -1,2 +1,19 @@ CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key BIGINT,dummy_entity BIGINT); + +CREATE TABLE root +( + id BIGINT AUTO_INCREMENT PRIMARY KEY +); +CREATE TABLE intermediate +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mssql.sql index a8d4104d42..a683a5b990 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mssql.sql @@ -2,3 +2,23 @@ DROP TABLE IF EXISTS dummy_entity; DROP TABLE IF EXISTS element; CREATE TABLE dummy_entity ( id BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT IDENTITY PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key BIGINT,dummy_entity BIGINT); + +DROP TABLE IF EXISTS root; +DROP TABLE IF EXISTS intermediate; +DROP TABLE IF EXISTS leaf; +CREATE TABLE root +( + id BIGINT IDENTITY PRIMARY KEY +); +CREATE TABLE intermediate +( + id BIGINT IDENTITY PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mysql.sql index 4cff749611..25c1e795ef 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-mysql.sql @@ -1,2 +1,19 @@ CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key BIGINT,dummy_entity BIGINT); + +CREATE TABLE root +( + id BIGINT AUTO_INCREMENT PRIMARY KEY +); +CREATE TABLE intermediate +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql index 47b211248d..18c40ae2d9 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql @@ -12,3 +12,24 @@ CREATE TABLE ELEMENT ( DUMMY_ENTITY_KEY NUMBER, DUMMY_ENTITY NUMBER ); + +DROP TABLE root; +DROP TABLE intermediate; +DROP TABLE leaf; + +CREATE TABLE root +( + id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY +); +CREATE TABLE intermediate +( + id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + root NUMBER NOT NULL, + root_key NUMBER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate NUMBER NOT NULL, + intermediate_key NUMBER NOT NULL +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-postgres.sql index c61ae2bd8e..0d9da1af3b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-postgres.sql @@ -2,3 +2,23 @@ DROP TABLE element; DROP TABLE dummy_entity; CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id SERIAL PRIMARY KEY, content VARCHAR(100),dummy_entity_key BIGINT, dummy_entity BIGINT); + +DROP TABLE root; +DROP TABLE intermediate; +DROP TABLE leaf; +CREATE TABLE root +( + id SERIAL PRIMARY KEY +); +CREATE TABLE intermediate +( + id SERIAL PRIMARY KEY, + root BIGINT NOT NULL, + root_key INTEGER NOT NULL +); +CREATE TABLE leaf +( + name VARCHAR(100), + intermediate BIGINT NOT NULL, + intermediate_key INTEGER NOT NULL +); \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index 490c9771b2..9191b8912e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java @@ -17,12 +17,14 @@ import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * An instance of this interface represents a (conceptual) single interaction with a database, e.g. a single update, @@ -33,6 +35,7 @@ * @author Mark Paluch * @author Tyler Van Gorder * @author Myeonghyeon Lee + * @author Chirag Tailor */ public interface DbAction { @@ -48,16 +51,19 @@ class Insert implements WithGeneratedId, WithDependingOn { private final T entity; private final PersistentPropertyPath propertyPath; private final WithEntity dependingOn; + private final IdValueSource idValueSource; final Map, Object> qualifiers; public Insert(T entity, PersistentPropertyPath propertyPath, - WithEntity dependingOn, Map, Object> qualifiers) { + WithEntity dependingOn, Map, Object> qualifiers, + IdValueSource idValueSource) { this.entity = entity; this.propertyPath = propertyPath; this.dependingOn = dependingOn; this.qualifiers = Collections.unmodifiableMap(new HashMap<>(qualifiers)); + this.idValueSource = idValueSource; } @Override @@ -81,9 +87,14 @@ public Map, Object> getQual return this.qualifiers; } + public IdValueSource getIdValueSource() { + return idValueSource; + } + + @Override public String toString() { - return "DbAction.Insert(entity=" + this.getEntity() + ", propertyPath=" + this.getPropertyPath() - + ", dependingOn=" + this.getDependingOn() + ", qualifiers=" + this.getQualifiers() + ")"; + return "Insert{" + "entity=" + entity + ", propertyPath=" + propertyPath + ", dependingOn=" + dependingOn + + ", idValueSource=" + idValueSource + ", qualifiers=" + qualifiers + '}'; } } @@ -96,17 +107,24 @@ public String toString() { class InsertRoot implements WithGeneratedId { private final T entity; + private final IdValueSource idValueSource; - public InsertRoot(T entity) { + public InsertRoot(T entity, IdValueSource idValueSource) { this.entity = entity; + this.idValueSource = idValueSource; } public T getEntity() { return this.entity; } + public IdValueSource getIdValueSource() { + return idValueSource; + } + + @Override public String toString() { - return "DbAction.InsertRoot(entity=" + this.getEntity() + ")"; + return "InsertRoot{" + "entity=" + entity + ", idValueSource=" + idValueSource + '}'; } } @@ -160,48 +178,6 @@ public String toString() { } } - /** - * Represents a merge statement for a single entity that is not the root of an aggregate. - * - * @param type of the entity for which this represents a database interaction. - */ - final class Merge implements WithDependingOn, WithPropertyPath { - - private final T entity; - private final PersistentPropertyPath propertyPath; - private final WithEntity dependingOn; - - private final Map, Object> qualifiers = Collections.emptyMap(); - - public Merge(T entity, PersistentPropertyPath propertyPath, - WithEntity dependingOn) { - this.entity = entity; - this.propertyPath = propertyPath; - this.dependingOn = dependingOn; - } - - public T getEntity() { - return this.entity; - } - - public PersistentPropertyPath getPropertyPath() { - return this.propertyPath; - } - - public DbAction.WithEntity getDependingOn() { - return this.dependingOn; - } - - public Map, Object> getQualifiers() { - return this.qualifiers; - } - - public String toString() { - return "DbAction.Merge(entity=" + this.getEntity() + ", propertyPath=" + this.getPropertyPath() + ", dependingOn=" - + this.getDependingOn() + ", qualifiers=" + this.getQualifiers() + ")"; - } - } - /** * Represents a delete statement for all entities that that a reachable via a give path from the aggregate root. * @@ -371,6 +347,41 @@ public String toString() { } } + /** + * Represents a batch insert statement for a multiple entities that are not aggregate roots. + * + * @param type of the entity for which this represents a database interaction. + * @since 2.4 + */ + final class InsertBatch implements DbAction { + private final List> inserts; + private final IdValueSource idValueSource; + + public InsertBatch(List> inserts, IdValueSource idValueSource) { + Assert.notEmpty(inserts, "Inserts must contains at least one insert"); + this.inserts = inserts; + this.idValueSource = idValueSource; + } + + @Override + public Class getEntityType() { + return inserts.get(0).getEntityType(); + } + + public List> getInserts() { + return inserts; + } + + public IdValueSource getIdValueSource() { + return idValueSource; + } + + @Override + public String toString() { + return "InsertBatch{" + "inserts=" + inserts + ", idValueSource=" + idValueSource + '}'; + } + } + /** * An action depending on another action for providing additional information like the id of a parent entity. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java new file mode 100644 index 0000000000..370ba2c049 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 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 + * + * https://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.springframework.data.relational.core.conversion; + +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * Enumeration describing the source of a value for an id property. + * + * @author Chirag Tailor + * @since 2.4 + */ +public enum IdValueSource { + + /** + * The id value is provided on the instance. + */ + PROVIDED, + + /** + * The id value is generated by the store. + */ + GENERATED, + + /** + * There is no id property, and therefore no id value source. + */ + NONE; + + /** + * Returns the appropriate {@link IdValueSource} for the instance: {@link IdValueSource#NONE} when the entity has no + * id property, {@link IdValueSource#PROVIDED} when the value of the id property is not null and when it is a + * primitive type, not zero, and {@link IdValueSource#GENERATED} otherwise. + */ + public static IdValueSource forInstance(Object instance, RelationalPersistentEntity persistentEntity) { + + Object idValue = persistentEntity.getIdentifierAccessor(instance).getIdentifier(); + RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); + if (idProperty == null) { + return IdValueSource.NONE; + } + boolean idPropertyValueIsSet = idValue != null && // + (idProperty.getType() != int.class || !idValue.equals(0)) // + && (idProperty.getType() != long.class || !idValue.equals(0L)); + if (idPropertyValueIsSet) { + return IdValueSource.PROVIDED; + } else { + return IdValueSource.GENERATED; + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java index 672e4bfa7e..59148979b1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java @@ -15,12 +15,10 @@ */ package org.springframework.data.relational.core.conversion; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import static java.util.Arrays.*; + +import java.util.*; +import java.util.stream.Collectors; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; @@ -38,6 +36,7 @@ * @author Bastian Wilhelm * @author Mark Paluch * @author Myeonghyeon Lee + * @author Chirag Tailor */ class WritingContext { @@ -48,6 +47,7 @@ class WritingContext { private final PersistentPropertyPaths paths; private final Map> previousActions = new HashMap<>(); private final Map, List> nodesCache = new HashMap<>(); + private final IdValueSource rootIdValueSource; WritingContext(RelationalMappingContext context, Object root, MutableAggregateChange aggregateChange) { @@ -55,6 +55,7 @@ class WritingContext { this.root = root; this.entity = aggregateChange.getEntity(); this.entityType = aggregateChange.getEntityType(); + this.rootIdValueSource = IdValueSource.forInstance(root, context.getRequiredPersistentEntity(aggregateChange.getEntityType())); this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded()); } @@ -67,7 +68,7 @@ class WritingContext { List> insert() { List> actions = new ArrayList<>(); - actions.add(setRootAction(new DbAction.InsertRoot<>(entity))); + actions.add(setRootAction(new DbAction.InsertRoot<>(entity, rootIdValueSource))); actions.addAll(insertReferenced()); return actions; } @@ -93,7 +94,7 @@ List> save() { List> actions = new ArrayList<>(); if (isNew(root)) { - actions.add(setRootAction(new DbAction.InsertRoot<>(entity))); + actions.add(setRootAction(new DbAction.InsertRoot<>(entity, rootIdValueSource))); actions.addAll(insertReferenced()); } else { @@ -121,18 +122,18 @@ private List> insertReferenced() { } @SuppressWarnings("unchecked") - private List> insertAll(PersistentPropertyPath path) { - - List> actions = new ArrayList<>(); + private List> insertAll(PersistentPropertyPath path) { + RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(path.getRequiredLeafProperty()); + List> inserts = new ArrayList<>(); from(path).forEach(node -> { DbAction.WithEntity parentAction = getAction(node.getParent()); - DbAction.Insert insert; + Map, Object> qualifiers = new HashMap<>(); + Object instance; if (node.getPath().getRequiredLeafProperty().isQualified()) { Pair value = (Pair) node.getValue(); - Map, Object> qualifiers = new HashMap<>(); qualifiers.put(node.getPath(), value.getFirst()); RelationalPersistentEntity parentEntity = context.getRequiredPersistentEntity(parentAction.getEntityType()); @@ -140,16 +141,27 @@ private List> insertAll(PersistentPropertyPath) parentAction).getQualifiers()); } - insert = new DbAction.Insert<>(value.getSecond(), path, parentAction, qualifiers); - + instance = value.getSecond(); } else { - insert = new DbAction.Insert<>(node.getValue(), path, parentAction, new HashMap<>()); + instance = node.getValue(); } + IdValueSource idValueSource = IdValueSource.forInstance(instance, persistentEntity); + DbAction.Insert insert = new DbAction.Insert<>(instance, path, parentAction, qualifiers, idValueSource); + inserts.add(insert); previousActions.put(node, insert); - actions.add(insert); }); - - return actions; + return inserts.stream() + .collect(Collectors.groupingBy(DbAction.Insert::getIdValueSource)) + .entrySet().stream() + .filter(entry -> (!entry.getValue().isEmpty())) + .map(entry -> { + List> batch = entry.getValue(); + if (batch.size() > 1) { + return new DbAction.InsertBatch<>(batch, entry.getKey()); + } + return batch.get(0); + }) + .collect(Collectors.toList()); } private List> deleteReferenced() { @@ -281,7 +293,7 @@ private List createNodes(PersistentPropertyPath nodes.add(new PathNode(path, parentNode, v))); + asList((Object[]) value).forEach(v -> nodes.add(new PathNode(path, parentNode, v))); } else { ((Iterable) value).forEach(v -> nodes.add(new PathNode(path, parentNode, v))); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java index 65620f7dc5..911d44718c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java @@ -34,8 +34,20 @@ public class Db2Dialect extends AbstractDialect { */ public static final Db2Dialect INSTANCE = new Db2Dialect(); + private static final IdGeneration ID_GENERATION = new IdGeneration() { + @Override + public boolean supportedForBatchOperations() { + return false; + } + }; + protected Db2Dialect() {} + @Override + public IdGeneration getIdGeneration() { + return ID_GENERATION; + } + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { /* diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java index a6e226f390..7b50e9a231 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java @@ -16,11 +16,13 @@ package org.springframework.data.relational.core.dialect; import java.sql.Connection; +import java.sql.PreparedStatement; /** - * Describes the how obtaining generated ids after an insert works for a given JDBC driver. + * Describes how obtaining generated ids after an insert works for a given JDBC driver. * * @author Jens Schauder + * @author Chirag Tailor * @since 2.1 */ public interface IdGeneration { @@ -42,4 +44,17 @@ public interface IdGeneration { default boolean driverRequiresKeyColumnNames() { return false; } + + /** + * Does the driver support id generation for batch operations. + *

+ * This should be {@literal true} for most dialects, except DB2 and SqlServer. + * + * @return {@literal true} if the JDBC driver supports generated keys for batch operations. + * @see PreparedStatement#getGeneratedKeys() + * @since 2.4 + */ + default boolean supportedForBatchOperations() { + return true; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java index 75a2be3fb7..617bab7ebd 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java @@ -36,8 +36,20 @@ public class SqlServerDialect extends AbstractDialect { */ public static final SqlServerDialect INSTANCE = new SqlServerDialect(); + private static final IdGeneration ID_GENERATION = new IdGeneration() { + @Override + public boolean supportedForBatchOperations() { + return false; + } + }; + protected SqlServerDialect() {} + @Override + public IdGeneration getIdGeneration() { + return ID_GENERATION; + } + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { /* diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java index 0e23382709..234a42b664 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java @@ -16,10 +16,12 @@ package org.springframework.data.relational.core.conversion; import lombok.experimental.UtilityClass; +import org.springframework.lang.Nullable; /** * Utility class for analyzing DbActions in tests. * @author Jens Schauder + * @author Chirag Tailor */ @UtilityClass class DbActionTestSupport { @@ -44,4 +46,17 @@ static Class actualEntityType(DbAction a) { } return null; } + + @Nullable + static IdValueSource insertIdValueSource(DbAction action) { + if (action instanceof DbAction.InsertRoot) { + return ((DbAction.InsertRoot) action).getIdValueSource(); + } else if (action instanceof DbAction.Insert) { + return ((DbAction.Insert) action).getIdValueSource(); + } else if (action instanceof DbAction.InsertBatch) { + return ((DbAction.InsertBatch) action).getIdValueSource(); + } else { + return null; + } + } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java index d73917d497..79d39d9c6a 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java @@ -17,15 +17,15 @@ import static org.assertj.core.api.Assertions.*; -import lombok.RequiredArgsConstructor; - import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -34,14 +34,18 @@ import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction.Delete; import org.springframework.data.relational.core.conversion.DbAction.Insert; +import org.springframework.data.relational.core.conversion.DbAction.InsertBatch; import org.springframework.data.relational.core.conversion.DbAction.InsertRoot; import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot; import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.lang.Nullable; +import lombok.Data; +import lombok.RequiredArgsConstructor; + /** * Unit tests for the {@link RelationalEntityWriter} * @@ -49,6 +53,7 @@ * @author Bastian Wilhelm * @author Mark Paluch * @author Myeonghyeon Lee + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityWriterUnitTests { @@ -89,9 +94,52 @@ public void newEntityGetsConvertedToOneInsert() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false, IdValueSource.GENERATED) // + ); + } + + @Test + void newEntityWithPrimitiveLongId_insertDoesNotIncludeId_whenIdValueIsZero() { + PrimitiveLongIdEntity entity = new PrimitiveLongIdEntity(); + + MutableAggregateChange aggregateChange = // + new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, PrimitiveLongIdEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(extractActions(aggregateChange)) // + .extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + tuple(InsertRoot.class, PrimitiveLongIdEntity.class, "", PrimitiveLongIdEntity.class, false, IdValueSource.GENERATED) // + ); + } + + @Test + void newEntityWithPrimitiveIntId_insertDoesNotIncludeId_whenIdValueIsZero() { + PrimitiveIntIdEntity entity = new PrimitiveIntIdEntity(); + + MutableAggregateChange aggregateChange = // + new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, PrimitiveIntIdEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(extractActions(aggregateChange)) // + .extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, PrimitiveIntIdEntity.class, "", PrimitiveIntIdEntity.class, false, IdValueSource.GENERATED) // ); } @@ -111,9 +159,10 @@ public void newEntityGetsConvertedToOneInsertByEmbeddedEntities() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, EmbeddedReferenceEntity.class, "", EmbeddedReferenceEntity.class, false) // + tuple(InsertRoot.class, EmbeddedReferenceEntity.class, "", EmbeddedReferenceEntity.class, false, IdValueSource.GENERATED) // ); } @@ -133,10 +182,37 @@ public void newEntityWithReferenceGetsConvertedToTwoInserts() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), // - tuple(Insert.class, Element.class, "other", Element.class, true) // + tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other", Element.class, true, IdValueSource.GENERATED) // + ); + } + + @Test + void newEntityWithReference_whenReferenceHasPrimitiveId_insertDoesNotIncludeId_whenIdValueIsZero() { + + EntityWithReferencesToPrimitiveIdEntity entity = new EntityWithReferencesToPrimitiveIdEntity(null); + entity.primitiveLongIdEntity = new PrimitiveLongIdEntity(); + entity.primitiveIntIdEntity = new PrimitiveIntIdEntity(); + + MutableAggregateChange aggregateChange = // + new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, EntityWithReferencesToPrimitiveIdEntity.class, entity); + + converter.write(entity, aggregateChange); + + assertThat(extractActions(aggregateChange)) // + .extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactlyInAnyOrder( // + tuple(InsertRoot.class, EntityWithReferencesToPrimitiveIdEntity.class, "", EntityWithReferencesToPrimitiveIdEntity.class, false, IdValueSource.GENERATED), // + tuple(Insert.class, PrimitiveLongIdEntity.class, "primitiveLongIdEntity", PrimitiveLongIdEntity.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, PrimitiveIntIdEntity.class, "primitiveIntIdEntity", PrimitiveIntIdEntity.class, true, IdValueSource.GENERATED) // ); } @@ -178,11 +254,12 @@ public void newReferenceTriggersDeletePlusInsert() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), // - tuple(Delete.class, Element.class, "other", null, false), // - tuple(Insert.class, Element.class, "other", Element.class, true) // + tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false, null), // + tuple(Delete.class, Element.class, "other", null, false, null), // + tuple(Insert.class, Element.class, "other", Element.class, true, IdValueSource.GENERATED) // ); } @@ -200,13 +277,14 @@ public void newEntityWithEmptySetResultsInSingleInsert() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false)); + tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false, IdValueSource.GENERATED)); } @Test // DATAJDBC-113 - public void newEntityWithSetResultsInAdditionalInsertPerElement() { + public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForTheBatch() { SetContainer entity = new SetContainer(null); entity.elements.add(new Element(null)); @@ -216,15 +294,27 @@ public void newEntityWithSetResultsInAdditionalInsertPerElement() { SetContainer.class, entity); converter.write(entity, aggregateChange); - assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false, IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // + ); + List> batchedInsertActions = getInsertBatchAction(actions, Element.class).getInserts(); + assertThat(batchedInsertActions).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false), // - tuple(Insert.class, Element.class, "elements", Element.class, true), // - tuple(Insert.class, Element.class, "elements", Element.class, true) // + tuple(Insert.class, Element.class, "elements", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "elements", Element.class, true, IdValueSource.GENERATED) // ); } @@ -248,21 +338,43 @@ public void cascadingReferencesTriggerCascadingActions() { converter.write(entity, aggregateChange); - assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false, IdValueSource.GENERATED), // + tuple(InsertBatch.class, CascadingReferenceMiddleElement.class, "", null, false, IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // + ); + List> middleElementInserts = getInsertBatchAction(actions, CascadingReferenceMiddleElement.class).getInserts(); + assertThat(middleElementInserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, - true), // + true, IdValueSource.GENERATED), // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, - true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true) // + true, IdValueSource.GENERATED) // + ); + List> leafElementInserts = getInsertBatchAction(actions, Element.class).getInserts(); + assertThat(leafElementInserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED) // ); } @@ -286,23 +398,46 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() { converter.write(entity, aggregateChange); - assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false, null), // + tuple(Delete.class, Element.class, "other.element", null, false, null), + tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false, null), + tuple(InsertBatch.class, CascadingReferenceMiddleElement.class, "", null, false, IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // + ); + List> middleElementInserts = getInsertBatchAction(actions, + CascadingReferenceMiddleElement.class).getInserts(); + assertThat(middleElementInserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), // - tuple(Delete.class, Element.class, "other.element", null, false), - tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false), tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, - true), // + true, IdValueSource.GENERATED), // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, - true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true), // - tuple(Insert.class, Element.class, "other.element", Element.class, true) // + true, IdValueSource.GENERATED) // + ); + List> elementInserts = getInsertBatchAction(actions, Element.class).getInserts(); + assertThat(elementInserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED) // ); } @@ -317,9 +452,10 @@ public void newEntityWithEmptyMapResultsInSingleInsert() { assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // DbAction::getEntityType, // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, MapContainer.class, "")); + tuple(InsertRoot.class, MapContainer.class, "", IdValueSource.GENERATED)); } @Test // DATAJDBC-131 @@ -333,20 +469,25 @@ public void newEntityWithMapResultsInAdditionalInsertPerElement() { MapContainer.class, entity); converter.write(entity, aggregateChange); - assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // DbAction::getEntityType, // this::getMapKey, // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, null, "", IdValueSource.GENERATED) // + ); + List> inserts = getInsertBatchAction(actions, Element.class).getInserts(); + assertThat(inserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + this::getMapKey, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactlyInAnyOrder( // - tuple(InsertRoot.class, MapContainer.class, null, ""), // - tuple(Insert.class, Element.class, "one", "elements"), // - tuple(Insert.class, Element.class, "two", "elements") // - ).containsSubsequence( // container comes before the elements - tuple(InsertRoot.class, MapContainer.class, null, ""), // - tuple(Insert.class, Element.class, "two", "elements") // - ).containsSubsequence( // container comes before the elements - tuple(InsertRoot.class, MapContainer.class, null, ""), // - tuple(Insert.class, Element.class, "one", "elements") // + tuple(Insert.class, Element.class, "one", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "two", "elements", IdValueSource.GENERATED) // ); } @@ -372,24 +513,35 @@ public void newEntityWithFullMapResultsInAdditionalInsertPerElement() { MapContainer.class, entity); converter.write(entity, aggregateChange); - assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // DbAction::getEntityType, // this::getMapKey, // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, null, "", IdValueSource.GENERATED) // + ); + List> inserts = getInsertBatchAction(actions, Element.class).getInserts(); + assertThat(inserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + this::getMapKey, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactlyInAnyOrder( // - tuple(InsertRoot.class, MapContainer.class, null, ""), // - tuple(Insert.class, Element.class, "1", "elements"), // - tuple(Insert.class, Element.class, "2", "elements"), // - tuple(Insert.class, Element.class, "3", "elements"), // - tuple(Insert.class, Element.class, "4", "elements"), // - tuple(Insert.class, Element.class, "5", "elements"), // - tuple(Insert.class, Element.class, "6", "elements"), // - tuple(Insert.class, Element.class, "7", "elements"), // - tuple(Insert.class, Element.class, "8", "elements"), // - tuple(Insert.class, Element.class, "9", "elements"), // - tuple(Insert.class, Element.class, "0", "elements"), // - tuple(Insert.class, Element.class, "a", "elements"), // - tuple(Insert.class, Element.class, "b", "elements") // + tuple(Insert.class, Element.class, "1", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "2", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "3", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "4", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "5", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "6", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "7", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "8", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "9", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "0", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "a", "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "b", "elements", IdValueSource.GENERATED) // ); } @@ -404,9 +556,10 @@ public void newEntityWithEmptyListResultsInSingleInsert() { assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // DbAction::getEntityType, // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, ListContainer.class, "")); + tuple(InsertRoot.class, ListContainer.class, "", IdValueSource.GENERATED)); } @Test // DATAJDBC-130 @@ -420,20 +573,25 @@ public void newEntityWithListResultsInAdditionalInsertPerElement() { ListContainer.class, entity); converter.write(entity, aggregateChange); - assertThat(extractActions(aggregateChange)).extracting(DbAction::getClass, // + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // DbAction::getEntityType, // this::getListKey, // - DbActionTestSupport::extractPath) // - .containsExactlyInAnyOrder( // - tuple(InsertRoot.class, ListContainer.class, null, ""), // - tuple(Insert.class, Element.class, 0, "elements"), // - tuple(Insert.class, Element.class, 1, "elements") // - ).containsSubsequence( // container comes before the elements - tuple(InsertRoot.class, ListContainer.class, null, ""), // - tuple(Insert.class, Element.class, 1, "elements") // - ).containsSubsequence( // container comes before the elements - tuple(InsertRoot.class, ListContainer.class, null, ""), // - tuple(Insert.class, Element.class, 0, "elements") // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(InsertRoot.class, ListContainer.class, null, "", IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, null, "", IdValueSource.GENERATED) // + ); + List> inserts = getInsertBatchAction(actions, Element.class).getInserts(); + assertThat(inserts).extracting(DbAction::getClass, // + DbAction::getEntityType, // + this::getListKey, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(Insert.class, Element.class, 0, "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, 1, "elements", IdValueSource.GENERATED) // ); } @@ -452,11 +610,12 @@ public void mapTriggersDeletePlusInsert() { .extracting(DbAction::getClass, // DbAction::getEntityType, // this::getMapKey, // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, MapContainer.class, null, ""), // - tuple(Delete.class, Element.class, null, "elements"), // - tuple(Insert.class, Element.class, "one", "elements") // + tuple(UpdateRoot.class, MapContainer.class, null, "", null), // + tuple(Delete.class, Element.class, null, "elements", null), // + tuple(Insert.class, Element.class, "one", "elements", IdValueSource.GENERATED) // ); } @@ -475,11 +634,12 @@ public void listTriggersDeletePlusInsert() { .extracting(DbAction::getClass, // DbAction::getEntityType, // this::getListKey, // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, ListContainer.class, null, ""), // - tuple(Delete.class, Element.class, null, "elements"), // - tuple(Insert.class, Element.class, 0, "elements") // + tuple(UpdateRoot.class, ListContainer.class, null, "", null), // + tuple(Delete.class, Element.class, null, "elements", null), // + tuple(Insert.class, Element.class, 0, "elements", IdValueSource.GENERATED) // ); } @@ -500,13 +660,14 @@ public void multiLevelQualifiedReferencesWithId() { DbAction::getEntityType, // a -> getQualifier(a, listMapContainerMaps), // a -> getQualifier(a, listMapContainerElements), // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, ListMapContainer.class, null, null, ""), // - tuple(Delete.class, Element.class, null, null, "maps.elements"), // - tuple(Delete.class, MapContainer.class, null, null, "maps"), // - tuple(Insert.class, MapContainer.class, 0, null, "maps"), // - tuple(Insert.class, Element.class, null, "one", "maps.elements") // + tuple(UpdateRoot.class, ListMapContainer.class, null, null, "", null), // + tuple(Delete.class, Element.class, null, null, "maps.elements", null), // + tuple(Delete.class, MapContainer.class, null, null, "maps", null), // + tuple(Insert.class, MapContainer.class, 0, null, "maps", IdValueSource.PROVIDED), // + tuple(Insert.class, Element.class, null, "one", "maps.elements", IdValueSource.GENERATED) // ); } @@ -527,13 +688,14 @@ public void multiLevelQualifiedReferencesWithOutId() { DbAction::getEntityType, // a -> getQualifier(a, noIdListMapContainerMaps), // a -> getQualifier(a, noIdListMapContainerElements), // - DbActionTestSupport::extractPath) // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, ""), // - tuple(Delete.class, NoIdElement.class, null, null, "maps.elements"), // - tuple(Delete.class, NoIdMapContainer.class, null, null, "maps"), // - tuple(Insert.class, NoIdMapContainer.class, 0, null, "maps"), // - tuple(Insert.class, NoIdElement.class, 0, "one", "maps.elements") // + tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, "", null), // + tuple(Delete.class, NoIdElement.class, null, null, "maps.elements", null), // + tuple(Delete.class, NoIdMapContainer.class, null, null, "maps", null), // + tuple(Insert.class, NoIdMapContainer.class, 0, null, "maps", IdValueSource.NONE), // + tuple(Insert.class, NoIdElement.class, 0, "one", "maps.elements", IdValueSource.NONE) // ); } @@ -553,9 +715,10 @@ public void savingANullEmbeddedWithEntity() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, EmbeddedReferenceChainEntity.class, "", EmbeddedReferenceChainEntity.class, false) // + tuple(InsertRoot.class, EmbeddedReferenceChainEntity.class, "", EmbeddedReferenceChainEntity.class, false, IdValueSource.GENERATED) // ); } @@ -576,11 +739,89 @@ public void savingInnerNullEmbeddedWithEntity() { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // tuple(InsertRoot.class, RootWithEmbeddedReferenceChainEntity.class, "", - RootWithEmbeddedReferenceChainEntity.class, false), // - tuple(Insert.class, EmbeddedReferenceChainEntity.class, "other", EmbeddedReferenceChainEntity.class, true) // + RootWithEmbeddedReferenceChainEntity.class, false, IdValueSource.GENERATED), // + tuple(Insert.class, EmbeddedReferenceChainEntity.class, "other", EmbeddedReferenceChainEntity.class, true, + IdValueSource.GENERATED) // + ); + } + + @Test + void newEntityWithCollectionWhereSomeElementsHaveIdSet_producesABatchInsertEachForElementsWithIdAndWithout() { + + ListContainer root = new ListContainer(null); + root.elements.add(new Element(null)); + root.elements.add(new Element(1L)); + root.elements.add(new Element(null)); + root.elements.add(new Element(2L)); + MutableAggregateChange aggregateChange = // + new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, ListContainer.class, root); + + converter.write(root, aggregateChange); + + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsSubsequence( + tuple(InsertRoot.class, ListContainer.class, "", ListContainer.class, false, IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.PROVIDED) // + ) + .containsSubsequence( // + tuple(InsertRoot.class, ListContainer.class, "", ListContainer.class, false, IdValueSource.GENERATED), // + tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // + ); + InsertBatch insertBatchWithoutId = getInsertBatchAction(actions, Element.class, IdValueSource.GENERATED); + assertThat(insertBatchWithoutId.getInserts()).extracting(DbAction::getClass, // + DbAction::getEntityType, // + this::getListKey, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(Insert.class, Element.class, 0, "elements", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, 2, "elements", IdValueSource.GENERATED) // + ); + InsertBatch insertBatchWithId = getInsertBatchAction(actions, Element.class, IdValueSource.PROVIDED); + assertThat(insertBatchWithId.getInserts()).extracting(DbAction::getClass, // + DbAction::getEntityType, // + this::getListKey, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::insertIdValueSource) // + .containsExactly( // + tuple(Insert.class, Element.class, 1, "elements", IdValueSource.PROVIDED), // + tuple(Insert.class, Element.class, 3, "elements", IdValueSource.PROVIDED) // + ); + } + + @Test + void newEntityWithCollection_whenElementHasPrimitiveId_batchInsertDoesNotIncludeId_whenIdValueIsZero() { + + EntityWithReferencesToPrimitiveIdEntity entity = new EntityWithReferencesToPrimitiveIdEntity(null); + entity.primitiveLongIdEntities.add(new PrimitiveLongIdEntity()); + entity.primitiveIntIdEntities.add(new PrimitiveIntIdEntity()); + + MutableAggregateChange aggregateChange = // + new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, EntityWithReferencesToPrimitiveIdEntity.class, entity); + + converter.write(entity, aggregateChange); + + List> actions = extractActions(aggregateChange); + assertThat(actions).extracting(DbAction::getClass, // + DbAction::getEntityType, // + DbActionTestSupport::extractPath, // + DbActionTestSupport::actualEntityType, // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // + .containsExactlyInAnyOrder( // + tuple(InsertRoot.class, EntityWithReferencesToPrimitiveIdEntity.class, "", EntityWithReferencesToPrimitiveIdEntity.class, false, IdValueSource.GENERATED), // + tuple(Insert.class, PrimitiveLongIdEntity.class, "primitiveLongIdEntities", PrimitiveLongIdEntity.class, true, IdValueSource.GENERATED), // + tuple(Insert.class, PrimitiveIntIdEntity.class, "primitiveIntIdEntities", PrimitiveIntIdEntity.class, true, IdValueSource.GENERATED) // ); } @@ -591,6 +832,32 @@ private List> extractActions(MutableAggregateChange aggregateChan return actions; } + @NotNull + private InsertBatch getInsertBatchAction(List> actions, Class entityType) { + return getInsertBatchActions(actions, entityType).stream() + .findFirst() + .orElseThrow(() -> new RuntimeException("No InsertBatch action found!")); + } + + @NotNull + private InsertBatch getInsertBatchAction(List> actions, Class entityType, + IdValueSource idValueSource) { + return getInsertBatchActions(actions, entityType).stream() + .filter(insertBatch -> insertBatch.getIdValueSource() == idValueSource) + .findFirst() + .orElseThrow(() -> new RuntimeException(String.format("No InsertBatch with includeId '%s' found!", idValueSource))); + } + + @NotNull + private List> getInsertBatchActions(List> actions, Class entityType) { + //noinspection unchecked + return actions.stream() // + .filter(dbAction -> dbAction instanceof InsertBatch) // + .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // + .map(dbAction -> (InsertBatch) dbAction) + .collect(Collectors.toList()); + } + private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) { CascadingReferenceMiddleElement middleElement1 = new CascadingReferenceMiddleElement(null); @@ -630,6 +897,26 @@ static PersistentPropertyPath toPath(String path, return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst().orElse(null); } + @RequiredArgsConstructor + @Data + static class EntityWithReferencesToPrimitiveIdEntity { + @Id final Long id; + PrimitiveLongIdEntity primitiveLongIdEntity; + List primitiveLongIdEntities = new ArrayList<>(); + PrimitiveIntIdEntity primitiveIntIdEntity; + List primitiveIntIdEntities = new ArrayList<>(); + } + + @Data + static class PrimitiveLongIdEntity { + @Id long id; + } + + @Data + static class PrimitiveIntIdEntity { + @Id int id; + } + @RequiredArgsConstructor static class SingleReferenceEntity { @@ -739,5 +1026,4 @@ private static class NoIdElement { // empty classes feel weird. String name; } - }