From 226bb71708d4a4df3b894ae4e9bba3b0988fd348 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Mon, 7 Feb 2022 12:15:48 -0600 Subject: [PATCH] Batch inserts of referenced entities. Insert for entities of same type within an aggregate get inserted using JDBC batch operations when possible. Inserts are supported when no id needs to be generated by the database or if the Dialect supports generation of ids in batch operations. Closes #1159 Original pull request # 1191 --- .../jdbc/core/AggregateChangeExecutor.java | 5 +- .../JdbcAggregateChangeExecutionContext.java | 37 +- .../core/convert/BatchInsertStrategy.java | 36 ++ .../core/convert/BatchJdbcOperations.java | 165 ++++++ .../convert/CascadingDataAccessStrategy.java | 14 +- .../jdbc/core/convert/DataAccessStrategy.java | 44 +- .../convert/DefaultDataAccessStrategy.java | 335 ++---------- .../convert/DelegatingDataAccessStrategy.java | 16 +- .../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 | 36 +- .../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 | 21 +- ...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, 2754 insertions(+), 790 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 ed227fe3..85b03407 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 @@ import org.springframework.lang.Nullable; * * @author Jens Schauder * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 2.0 */ class AggregateChangeExecutor { @@ -66,6 +67,8 @@ class AggregateChangeExecutor { 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 522af4f2..1c64958e 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.List; 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.DataAccessStrategy; 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 @@ import org.springframework.util.Assert; * @author Jens Schauder * @author Umut Erturk * @author Myeonghyeon Lee + * @author Chirag Tailor */ class JdbcAggregateChangeExecutionContext { @@ -87,11 +90,11 @@ class JdbcAggregateChangeExecutionContext { 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 @@ class JdbcAggregateChangeExecutionContext { 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 @@ class JdbcAggregateChangeExecutionContext { 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 00000000..2d01cfe6 --- /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 00000000..0a2cfbc0 --- /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 c79de971..7987fabf 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 java.util.function.Function; 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 @@ import org.springframework.data.relational.core.sql.LockMode; * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 1.1 */ public class CascadingDataAccessStrategy implements DataAccessStrategy { @@ -50,6 +52,16 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy { return collect(das -> das.insert(instance, domainType, identifier)); } + @Override + 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)); + } + @Override public boolean update(S instance, Class domainType) { return collect(das -> das.update(instance, domainType)); 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 8ab50d3a..0ffc19c8 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.Pageable; 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 @@ import org.springframework.lang.Nullable; * @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 d839e843..cd7f05f2 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 @@ package org.springframework.data.jdbc.core.convert; 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.data.relational.core.sql.SqlIdentifier; 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 @@ import org.springframework.util.Assert; * @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,76 +77,56 @@ 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) { - 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()); - } - - String insertSql = sqlGenerator.getInsert(new HashSet<>(parameterSource.getIdentifiers())); - - if (idValue == null) { - return executeInsertAndReturnGeneratedId(domainType, persistentEntity, parameterSource, insertSql); - } else { - - operations.update(insertSql, parameterSource); - return null; - } + RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(domainType); + return insert(instance, domainType, identifier, IdValueSource.forInstance(instance, persistentEntity)); } - @Nullable - private Object executeInsertAndReturnGeneratedId(Class domainType, RelationalPersistentEntity persistentEntity, SqlIdentifierParameterSource parameterSource, String insertSql) { + @Override + public Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { - KeyHolder holder = new GeneratedKeyHolder(); + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forInsert(instance, domainType, identifier, idValueSource); - IdGeneration idGeneration = sqlGeneratorSource.getDialect().getIdGeneration(); + String insertSql = sql(domainType).getInsert(parameterSource.getIdentifiers()); - if (idGeneration.driverRequiresKeyColumnNames()) { + return insertStrategyFactory.insertStrategy(idValueSource, getIdColumn(domainType)).execute(insertSql, parameterSource); + } - 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); - } + @Override + public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { - return getIdFromHolder(holder, persistentEntity); + 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 insertSql = sql(domainType).getInsert(sqlParameterSources[0].getIdentifiers()); + + return insertStrategyFactory.batchInsertStrategy(idValueSource, getIdColumn(domainType)).execute(insertSql, sqlParameterSources); } @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; } @Override @@ -165,8 +135,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); @@ -184,7 +153,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); } @@ -196,7 +165,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); @@ -217,13 +186,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { String delete = sql(rootEntity.getType()).createDeleteByPath(propertyPath); - SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(getIdentifierProcessing()); - addConvertedPropertyValue( // - parameters, // - rootEntity.getRequiredIdProperty(), // - rootId, // - ROOT_ID_PARAMETER // - ); + SqlIdentifierParameterSource parameters = sqlParametersFactory.forQueryById(rootId, rootEntity.getType(), ROOT_ID_PARAMETER); operations.update(delete, parameters); } @@ -242,7 +205,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); } @@ -269,7 +232,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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)); @@ -292,10 +255,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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(); @@ -319,25 +279,15 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); } @Override 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"); @@ -357,87 +307,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); } @@ -454,72 +323,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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); @@ -529,60 +336,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { 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 0db6f4a1..fb2de8d8 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 @@ package org.springframework.data.jdbc.core.convert; 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,6 +33,7 @@ import org.springframework.util.Assert; * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 1.1 */ public class DelegatingDataAccessStrategy implements DataAccessStrategy { @@ -41,6 +45,16 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy { return delegate.insert(instance, domainType, identifier); } + @Override + 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); + } + @Override public boolean update(S instance, Class domainType) { return delegate.update(instance, domainType); 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 00000000..296f53c2 --- /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 00000000..5577b72c --- /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 00000000..2bd19e04 --- /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 00000000..7eaad167 --- /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 00000000..155bd3fb --- /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 a95c9b19..989d7345 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 @@ class SqlGenerator { 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 00000000..a182e44f --- /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 075f5efc..860b75cd 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 static java.util.Arrays.*; 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.apache.commons.logging.LogFactory; 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 @@ import org.springframework.util.Assert; * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor */ public class MyBatisDataAccessStrategy implements DataAccessStrategy { @@ -103,11 +100,15 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { 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); @@ -155,6 +156,23 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { return myBatisContext.getId(); } + @Override + public Object insert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { + + MyBatisContext myBatisContext = new MyBatisContext(identifier, instance, domainType); + sqlSession().insert(namespace(domainType) + ".insert", myBatisContext); + + 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(); + } + @Override public boolean update(S instance, Class domainType) { 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 2df71fe4..7b237e1b 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.core.convert.converter.Converter; 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.convert.JdbcArrayColumns; import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -61,6 +54,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; * @author Michael Simons * @author Christoph Strobl * @author Myeonghyeon Lee + * @author Chirag Tailor * @since 1.1 */ @Configuration(proxyBeanMethods = false) @@ -176,7 +170,8 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { 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 b66eba97..f6625fb5 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.BeanFactory; 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 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Mark Paluch * @author Hebert Coelho + * @author Chirag Tailor */ public class JdbcRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport implements ApplicationEventPublisherAware { @@ -177,8 +181,12 @@ public class JdbcRepositoryFactoryBean, S, ID extend 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 d845ecc5..f95843d0 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.jdbc.core.convert.JdbcConverter; 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 @@ import org.springframework.lang.Nullable; * * @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 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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 1d7086e3..ad520ff9 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.jdbc.core.convert.JdbcConverter; 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 @@ import org.springframework.lang.Nullable; * * @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 class AggregateChangeIdGenerationUnitTests { 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 @@ public class AggregateChangeIdGenerationUnitTests { 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 6e532c1f..ee522838 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.jdbc.core.convert.JdbcIdentifierBuilder; 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 class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 @@ public class JdbcAggregateChangeExecutorContextImmutableUnitTests { @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 afc4990f..f00a0256 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.DataAccessStrategy; 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 @@ import org.springframework.lang.Nullable; * * @author Jens Schauder * @author Umut Erturk + * @author Chirag Tailor */ public class JdbcAggregateChangeExecutorContextUnitTests { @@ -69,9 +73,9 @@ public class JdbcAggregateChangeExecutorContextUnitTests { @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 class JdbcAggregateChangeExecutorContextUnitTests { 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 class JdbcAggregateChangeExecutorContextUnitTests { 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 class JdbcAggregateChangeExecutorContextUnitTests { 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 class JdbcAggregateChangeExecutorContextUnitTests { 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 5098823c..2f2cf214 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 @@ import org.springframework.jdbc.support.KeyHolder; * @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 class DefaultDataAccessStrategyUnitTests { 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() { + @Test // GH-1159 + public void insert() { - additionalParameters.put(SqlIdentifier.quoted("ID"), ID_FROM_ADDITIONAL_VALUES); + accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.empty(), IdValueSource.PROVIDED); - accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.from(additionalParameters)); - - verify(namedJdbcOperations).update(eq("INSERT INTO \"DUMMY_ENTITY\" (\"ID\") VALUES (:ID)"), - paramSourceCaptor.capture()); + verify(insertStrategyFactory).insertStrategy(IdValueSource.PROVIDED, SqlIdentifier.quoted("ID")); } - @Test // DATAJDBC-146 - public void additionalParametersGetAddedToStatement() { + @Test // GH-1159 + public void batchInsert() { - ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + accessStrategy.insert(singletonList(InsertSubject.describedBy(new DummyEntity(ORIGINAL_ID), Identifier.empty())), DummyEntity.class, IdValueSource.PROVIDED); - 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); + verify(insertStrategyFactory).batchInsertStrategy(IdValueSource.PROVIDED, SqlIdentifier.quoted("ID")); } - @Test // DATAJDBC-235 - public void considersConfiguredWriteConverter() { + @Test // GH-1159 + public void insertForEntityWithNoId() { - DefaultDataAccessStrategy accessStrategy = createAccessStrategyWithConverter( - asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE)); + accessStrategy.insert(new DummyEntityWithoutIdAnnotation(ORIGINAL_ID), DummyEntityWithoutIdAnnotation.class, Identifier.empty(), IdValueSource.GENERATED); - ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); - - 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.GENERATED, null); } - @Test // DATAJDBC-412 - public void considersConfiguredWriteConverterForIdValueObjects() { + @Test // GH-1159 + public void batchInsertForEntityWithNoId() { - DefaultDataAccessStrategy accessStrategy = createAccessStrategyWithConverter( - singletonList(IdValueToStringConverter.INSTANCE)); + accessStrategy.insert(singletonList(InsertSubject.describedBy(new DummyEntityWithoutIdAnnotation(ORIGINAL_ID), Identifier.empty())), DummyEntityWithoutIdAnnotation.class, IdValueSource.GENERATED); - String rawId = "batman"; - - WithValueObjectId entity = new WithValueObjectId(new IdValue(rawId)); - entity.value = "vs. superman"; - - 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); - } - - @Test // DATAJDBC-349 - public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInOneToManyRelationship() { - - DefaultDataAccessStrategy accessStrategy = createAccessStrategyWithConverter( - 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); - - 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); - } - - @Test // GH-933 - public void insertWithDefinedIdDoesNotRetrieveGeneratedKeys() { - - Object generatedId = accessStrategy.insert(new DummyEntity(ORIGINAL_ID), DummyEntity.class, Identifier.from(additionalParameters)); - - assertThat(generatedId).isNull(); - - 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 @@ public class DefaultDataAccessStrategyUnitTests { @Id private final Long id; } - @RequiredArgsConstructor // DATAJDBC-349 - private static class DummyEntityRoot { + @RequiredArgsConstructor + private static class DummyEntityWithoutIdAnnotation { - @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; - - @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 00000000..57752830 --- /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 00000000..0e63b4f1 --- /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 00000000..6a6dcf97 --- /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 00000000..e7a5b56e --- /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 4e0a58b6..63377482 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.Identifier; 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 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing; * @author Jens Schauder * @author Mark Paluch * @author Tyler Van Gorder + * @author Chirag Tailor */ public class MyBatisDataAccessStrategyUnitTests { @@ -67,7 +69,7 @@ public class MyBatisDataAccessStrategyUnitTests { @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 ba1af86b..a31768b0 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.AssumeFeatureTestExecutionListener; 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 @@ import org.springframework.transaction.annotation.Transactional; * @author Jens Schauder * @author Thomas Lang * @author Yunyung LEE + * @author Chirag Tailor */ @ContextConfiguration @Transactional @@ -182,7 +183,7 @@ public class JdbcRepositoryPropertyConversionIntegrationTests { @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 70aa42f1..59d12cb4 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.context.annotation.Import; 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 @@ import org.springframework.transaction.annotation.Transactional; * Very simple use cases for creation and usage of JdbcRepositories. * * @author Jens Schauder + * @author Chirag Tailor */ @ContextConfiguration @Transactional @@ -76,7 +77,7 @@ public class JdbcRepositoryWithCollectionsAndManuallyAssignedIdHsqlIntegrationTe @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 32359926..5cb5d448 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.ArrayList; 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 @@ import org.springframework.transaction.annotation.Transactional; * * @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 class JdbcRepositoryWithListsIntegrationTests { .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 @@ public class JdbcRepositoryWithListsIntegrationTests { interface DummyEntityRepository extends CrudRepository {} + interface RootRepository extends CrudRepository {} + @Configuration @Import(TestConfiguration.class) static class Config { @@ -210,6 +240,11 @@ public class JdbcRepositoryWithListsIntegrationTests { DummyEntityRepository dummyEntityRepository() { return factory.getRepository(DummyEntityRepository.class); } + + @Bean + RootRepository rootRepository() { + return factory.getRepository(RootRepository.class); + } } @Data @@ -228,4 +263,20 @@ public class JdbcRepositoryWithListsIntegrationTests { @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 f0947562..580bc988 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.annotation.Id; 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.H2Dialect; 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.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.jdbc.core.JdbcOperations; @@ -69,6 +64,7 @@ import org.springframework.lang.Nullable; * @author Myeonghyeon Lee * @author Milan Milanov * @author Myeonghyeon Lee + * @author Chirag Tailor */ public class SimpleJdbcRepositoryEventsUnitTests { @@ -87,8 +83,12 @@ public class SimpleJdbcRepositoryEventsUnitTests { 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()); @@ -298,7 +298,8 @@ public class SimpleJdbcRepositoryEventsUnitTests { return operations; } - interface DummyEntityRepository extends CrudRepository, PagingAndSortingRepository {} + interface DummyEntityRepository + extends CrudRepository, PagingAndSortingRepository {} @Value @With 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 6cc79180..a94b535b 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.ComponentScan; 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 @@ import org.springframework.util.ReflectionUtils; * @author Greg Turnquist * @author Evgeni Dimitrov * @author Fei Dong + * @author Chirag Tailor */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = TestConfiguration.class) @@ -159,7 +163,8 @@ public class EnableJdbcRepositoriesIntegrationTests { @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 645db5b3..d2baea71 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.ComponentScan; 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.convert.JdbcArrayColumns; import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -66,6 +59,7 @@ import org.springframework.transaction.PlatformTransactionManager; * @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 @@ public class TestConfiguration { 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 5532bbff..2bcdedbd 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 73abf96c..4b9a10ac 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 73abf96c..7a9df99a 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 4cff7496..25c1e795 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 a8d4104d..a683a5b9 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 4cff7496..25c1e795 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 47b21124..18c40ae2 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 c61ae2bd..0d9da1af 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 f7c904d6..7d50adca 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 @@ package org.springframework.data.relational.core.conversion; 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 @@ import org.springframework.lang.Nullable; * @author Mark Paluch * @author Tyler Van Gorder * @author Myeonghyeon Lee + * @author Chirag Tailor */ public interface DbAction { @@ -48,16 +51,19 @@ public interface DbAction { 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 interface DbAction { 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 interface DbAction { 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 interface DbAction { } } - /** - * 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. * @@ -370,6 +346,41 @@ public interface DbAction { } } + /** + * 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 00000000..370ba2c0 --- /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 672e4bfa..59148979 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 @@ import org.springframework.util.Assert; * @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 @@ class WritingContext { 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 @@ class WritingContext { } @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 @@ class WritingContext { if (!parentEntity.hasIdProperty() && parentAction instanceof DbAction.Insert) { qualifiers.putAll(((DbAction.Insert) 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 @@ class WritingContext { } } else if (path.getRequiredLeafProperty().isCollectionLike()) { // collection value if (value.getClass().isArray()) { - Arrays.asList((Object[]) value).forEach(v -> 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 ca9d70a8..94ea8180 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() { @Override 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 a6e226f3..7b50e9a2 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 402ab0d0..5dd0e0f6 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() { @Override 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 0e233827..234a42b6 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 @@ class DbActionTestSupport { } 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 d73917d4..79d39d9c 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 @@ package org.springframework.data.relational.core.conversion; 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.PersistentPropertyPath; 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 @@ import org.springframework.lang.Nullable; * @author Bastian Wilhelm * @author Mark Paluch * @author Myeonghyeon Lee + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityWriterUnitTests { @@ -89,9 +94,52 @@ public class RelationalEntityWriterUnitTests { DbAction::getEntityType, // DbActionTestSupport::extractPath, // DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn) // + DbActionTestSupport::isWithDependsOn, // + DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // + 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, 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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), // - tuple(Insert.class, Element.class, "elements", Element.class, true), // - tuple(Insert.class, Element.class, "elements", Element.class, true) // + 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(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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { .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 class RelationalEntityWriterUnitTests { .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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 class RelationalEntityWriterUnitTests { 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 @@ public class RelationalEntityWriterUnitTests { 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 @@ public class RelationalEntityWriterUnitTests { 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 @@ public class RelationalEntityWriterUnitTests { // empty classes feel weird. String name; } - }