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
This commit is contained in:
committed by
Jens Schauder
parent
13b48993b2
commit
226bb71708
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
<T> void executeInsert(DbAction.Insert<T> 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));
|
||||
}
|
||||
|
||||
<T> void executeInsertBatch(DbAction.InsertBatch<T> insertBatch) {
|
||||
|
||||
List<DbAction.Insert<T>> inserts = insertBatch.getInserts();
|
||||
List<InsertSubject<T>> 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));
|
||||
}
|
||||
}
|
||||
|
||||
<T> void executeUpdateRoot(DbAction.UpdateRoot<T> update) {
|
||||
|
||||
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(update.getEntityType());
|
||||
@@ -155,18 +172,6 @@ class JdbcAggregateChangeExecutionContext {
|
||||
accessStrategy.deleteAll(delete.getPropertyPath());
|
||||
}
|
||||
|
||||
<T> void executeMerge(DbAction.Merge<T> 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());
|
||||
}
|
||||
}
|
||||
|
||||
<T> void executeAcquireLock(DbAction.AcquireLockRoot<T> acquireLock) {
|
||||
accessStrategy.acquireLockById(acquireLock.getId(), LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<SqlParameter> 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<int[]> 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<Integer> 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<Map<String, Object>> generatedKeys = generatedKeyHolder.getKeyList();
|
||||
ResultSet keys = ps.getGeneratedKeys();
|
||||
if (keys != null) {
|
||||
try {
|
||||
RowMapperResultSetExtractor<Map<String, Object>> rse =
|
||||
new RowMapperResultSetExtractor<>(new ColumnMapRowMapper(), rowsExpected);
|
||||
//noinspection ConstantConditions
|
||||
generatedKeys.addAll(rse.extractData(keys));
|
||||
}
|
||||
finally {
|
||||
JdbcUtils.closeResultSet(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <T> Object insert(T instance, Class<T> domainType, Identifier identifier, IdValueSource idValueSource) {
|
||||
return collect(das -> das.insert(instance, domainType, identifier, idValueSource));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> domainType, IdValueSource idValueSource) {
|
||||
return collect(das -> das.insert(insertSubjects, domainType, idValueSource));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> boolean update(S instance, Class<S> domainType) {
|
||||
return collect(das -> das.update(instance, domainType));
|
||||
|
||||
@@ -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 <T> 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 <T> 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
|
||||
<T> Object insert(T instance, Class<T> domainType, Identifier identifier);
|
||||
|
||||
/**
|
||||
* Inserts the data of a single entity. Referenced entities don't get handled.
|
||||
*
|
||||
* @param <T> 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
|
||||
<T> Object insert(T instance, Class<T> domainType, Identifier identifier, IdValueSource idValueSource);
|
||||
|
||||
/**
|
||||
* Inserts the data of a multiple entities.
|
||||
*
|
||||
* @param <T> 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
|
||||
*/
|
||||
<T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> domainType, IdValueSource idValueSource);
|
||||
|
||||
/**
|
||||
* Updates the data of a single entity in the database. Referenced entities don't get handled.
|
||||
*
|
||||
|
||||
@@ -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 <T> Object insert(T instance, Class<T> domainType, Identifier identifier) {
|
||||
|
||||
SqlGenerator sqlGenerator = sql(domainType);
|
||||
RelationalPersistentEntity<T> 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 <T> Object executeInsertAndReturnGeneratedId(Class<T> domainType, RelationalPersistentEntity<T> persistentEntity, SqlIdentifierParameterSource parameterSource, String insertSql) {
|
||||
@Override
|
||||
public <T> Object insert(T instance, Class<T> 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 <T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> 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 <S> boolean update(S instance, Class<S> domainType) {
|
||||
|
||||
RelationalPersistentEntity<S> 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<S> 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<T> 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 <T> void acquireLockById(Object id, LockMode lockMode, Class<T> 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> T findById(Object id, Class<T> 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<T>) 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<Object>) 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<Object>) rowMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> boolean existsById(Object id, Class<T> 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<T>) getEntityRowMapper(domainType));
|
||||
}
|
||||
|
||||
private <S, T> SqlIdentifierParameterSource getParameterSource(@Nullable S instance,
|
||||
RelationalPersistentEntity<S> persistentEntity, String prefix,
|
||||
Predicate<RelationalPersistentProperty> skipProperty, IdentifierProcessing identifierProcessing) {
|
||||
|
||||
SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(identifierProcessing);
|
||||
|
||||
PersistentPropertyAccessor<S> 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<T>) 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 <S, ID> ID getIdValueOrNull(S instance, RelationalPersistentEntity<S> persistentEntity) {
|
||||
|
||||
ID idValue = (ID) persistentEntity.getIdentifierAccessor(instance).getIdentifier();
|
||||
|
||||
return isIdPropertyNullOrScalarZero(idValue, persistentEntity) ? null : idValue;
|
||||
}
|
||||
|
||||
private static <S, ID> boolean isIdPropertyNullOrScalarZero(@Nullable ID idValue,
|
||||
RelationalPersistentEntity<S> 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 <S> Object getIdFromHolder(KeyHolder holder, RelationalPersistentEntity<S> 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<String, Object> 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 <T> SqlIdentifierParameterSource createIdParameterSource(Object id, Class<T> 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<Object> 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 <S> RelationalPersistentEntity<S> getRequiredPersistentEntity(Class<S> domainType) {
|
||||
return (RelationalPersistentEntity<S>) context.getRequiredPersistentEntity(domainType);
|
||||
@@ -529,60 +336,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
return sqlGeneratorSource.getSqlGenerator(domainType);
|
||||
}
|
||||
|
||||
private <T> String[] getKeyColumnNames(Class<T> 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<RelationalPersistentProperty> includeAll() {
|
||||
return it -> false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link PersistentPropertyAccessor} implementation always returning null
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
static class NoValuePropertyAccessor<T> implements PersistentPropertyAccessor<T> {
|
||||
|
||||
private static final NoValuePropertyAccessor INSTANCE = new NoValuePropertyAccessor();
|
||||
|
||||
static <T> NoValuePropertyAccessor<T> 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 <T> SqlIdentifier getIdColumn(Class<T> domainType) {
|
||||
return Optional.ofNullable(context.getRequiredPersistentEntity(domainType).getIdProperty())
|
||||
.map(RelationalPersistentProperty::getColumnName)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> Object insert(T instance, Class<T> domainType, Identifier identifier, IdValueSource idValueSource) {
|
||||
return delegate.insert(instance, domainType, identifier, idValueSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> domainType, IdValueSource idValueSource) {
|
||||
return delegate.insert(insertSubjects, domainType, idValueSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> boolean update(S instance, Class<S> domainType) {
|
||||
return delegate.update(instance, domainType);
|
||||
|
||||
@@ -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<Map<String, Object>> keyList = holder.getKeyList();
|
||||
for (int i = 0; i < keyList.size(); i++) {
|
||||
Map<String, Object> 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]);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<T> {
|
||||
private final T instance;
|
||||
private final Identifier identifier;
|
||||
|
||||
public static <T> InsertSubject<T> 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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
<T> SqlIdentifierParameterSource forInsert(T instance, Class<T> domainType, Identifier identifier, IdValueSource idValueSource) {
|
||||
|
||||
RelationalPersistentEntity<T> 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
|
||||
*/
|
||||
<T> SqlIdentifierParameterSource forUpdate(T instance, Class<T> 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
|
||||
*/
|
||||
<T> SqlIdentifierParameterSource forQueryById(Object id, Class<T> 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
|
||||
*/
|
||||
<T> SqlIdentifierParameterSource forQueryByIds(Iterable<?> ids, Class<T> 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<RelationalPersistentProperty> 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<Object> 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 <S> RelationalPersistentEntity<S> getRequiredPersistentEntity(Class<S> domainType) {
|
||||
return (RelationalPersistentEntity<S>) context.getRequiredPersistentEntity(domainType);
|
||||
}
|
||||
|
||||
private <S, T> SqlIdentifierParameterSource getParameterSource(@Nullable S instance,
|
||||
RelationalPersistentEntity<S> persistentEntity, String prefix,
|
||||
Predicate<RelationalPersistentProperty> skipProperty, IdentifierProcessing identifierProcessing) {
|
||||
|
||||
SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(identifierProcessing);
|
||||
|
||||
PersistentPropertyAccessor<S> 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<T>) 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 <T>
|
||||
*/
|
||||
static class NoValuePropertyAccessor<T> implements PersistentPropertyAccessor<T> {
|
||||
|
||||
private static final NoValuePropertyAccessor INSTANCE = new NoValuePropertyAccessor();
|
||||
|
||||
static <T> NoValuePropertyAccessor<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <T> Object insert(T instance, Class<T> domainType, Identifier identifier, IdValueSource idValueSource) {
|
||||
|
||||
MyBatisContext myBatisContext = new MyBatisContext(identifier, instance, domainType);
|
||||
sqlSession().insert(namespace(domainType) + ".insert", myBatisContext);
|
||||
|
||||
return myBatisContext.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Object[] insert(List<InsertSubject<T>> insertSubjects, Class<T> domainType, IdValueSource idValueSource) {
|
||||
|
||||
return insertSubjects.stream()
|
||||
.map(insertSubject -> insert(insertSubject.getInstance(), domainType, insertSubject.getIdentifier(), idValueSource))
|
||||
.toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> boolean update(S instance, Class<S> domainType) {
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<T extends Repository<S, ID>, S, ID extends Serializable>
|
||||
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> implements ApplicationEventPublisherAware {
|
||||
@@ -177,8 +181,12 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Object> 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<RelationalPersistentProperty> propertyPath = toPath(
|
||||
parentInsert.getPropertyPath().toDotPath() + "." + propertyName);
|
||||
DbAction.Insert<Object> insert = new DbAction.Insert<>(value, propertyPath, parentInsert,
|
||||
singletonMap(propertyPath, key));
|
||||
singletonMap(propertyPath, key), IdValueSource.GENERATED);
|
||||
|
||||
return insert;
|
||||
}
|
||||
|
||||
@@ -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<RelationalPersistentProperty> toPath(String path) {
|
||||
|
||||
@@ -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<DummyEntity> rootInsert = new DbAction.InsertRoot<>(root);
|
||||
DbAction.InsertRoot<DummyEntity> 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<DummyEntity> rootInsert = new DbAction.InsertRoot<>(root);
|
||||
DbAction.InsertRoot<DummyEntity> 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<Object> 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;
|
||||
}
|
||||
|
||||
@@ -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<DummyEntity> rootInsert = new DbAction.InsertRoot<>(root);
|
||||
DbAction.InsertRoot<DummyEntity> 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<DummyEntity> rootInsert = new DbAction.InsertRoot<>(root);
|
||||
DbAction.InsertRoot<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<Object> 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;
|
||||
}
|
||||
|
||||
@@ -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<SqlIdentifier, Object> additionalParameters = new HashMap<>();
|
||||
ArgumentCaptor<SqlParameterSource> 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<String> 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<String> 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<RelationalPersistentProperty> 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<DummyEntity> 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<Boolean, String> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public String convert(Boolean source) {
|
||||
return source != null && source ? "T" : "F";
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToBooleanConverter implements Converter<String, Boolean> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Boolean convert(String source) {
|
||||
return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum IdValueToStringConverter implements Converter<IdValue, String> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public String convert(IdValue source) {
|
||||
return source.id;
|
||||
}
|
||||
private final Long id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<SqlIdentifier, Object> 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<IdValue, String> {
|
||||
|
||||
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<Boolean, String> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public String convert(Boolean source) {
|
||||
return source != null && source ? "T" : "F";
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToBooleanConverter implements Converter<String, Boolean> {
|
||||
|
||||
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<DummyEntity> 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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<BeforeSaveEvent>) beforeInsert -> ((EntityWithColumnsRequiringConversions) beforeInsert
|
||||
return (ApplicationListener<BeforeConvertEvent>) event -> ((EntityWithColumnsRequiringConversions) event
|
||||
.getEntity()).setIdTimestamp(getNow());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BeforeSaveEvent>) event -> {
|
||||
return (ApplicationListener<BeforeConvertEvent>) event -> {
|
||||
|
||||
if (event.getEntity() instanceof DummyEntity) {
|
||||
setIds((DummyEntity) event.getEntity());
|
||||
|
||||
@@ -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<DummyEntity, Long> {}
|
||||
|
||||
interface RootRepository extends CrudRepository<Root, Long> {}
|
||||
|
||||
@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<Intermediate> intermediates = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
static class Intermediate {
|
||||
@Id private Long id;
|
||||
List<Leaf> leaves = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Leaf {
|
||||
String name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DummyEntity, Long>, PagingAndSortingRepository<DummyEntity, Long> {}
|
||||
interface DummyEntityRepository
|
||||
extends CrudRepository<DummyEntity, Long>, PagingAndSortingRepository<DummyEntity, Long> {}
|
||||
|
||||
@Value
|
||||
@With
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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<T> {
|
||||
|
||||
@@ -48,16 +51,19 @@ public interface DbAction<T> {
|
||||
private final T entity;
|
||||
private final PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
|
||||
private final WithEntity<?> dependingOn;
|
||||
private final IdValueSource idValueSource;
|
||||
|
||||
final Map<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifiers;
|
||||
|
||||
public Insert(T entity, PersistentPropertyPath<RelationalPersistentProperty> propertyPath,
|
||||
WithEntity<?> dependingOn, Map<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifiers) {
|
||||
WithEntity<?> dependingOn, Map<PersistentPropertyPath<RelationalPersistentProperty>, 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<T> {
|
||||
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<T> {
|
||||
class InsertRoot<T> implements WithGeneratedId<T> {
|
||||
|
||||
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<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a merge statement for a single entity that is not the root of an aggregate.
|
||||
*
|
||||
* @param <T> type of the entity for which this represents a database interaction.
|
||||
*/
|
||||
final class Merge<T> implements WithDependingOn<T>, WithPropertyPath<T> {
|
||||
|
||||
private final T entity;
|
||||
private final PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
|
||||
private final WithEntity<?> dependingOn;
|
||||
|
||||
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifiers = Collections.emptyMap();
|
||||
|
||||
public Merge(T entity, PersistentPropertyPath<RelationalPersistentProperty> propertyPath,
|
||||
WithEntity<?> dependingOn) {
|
||||
this.entity = entity;
|
||||
this.propertyPath = propertyPath;
|
||||
this.dependingOn = dependingOn;
|
||||
}
|
||||
|
||||
public T getEntity() {
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
public PersistentPropertyPath<RelationalPersistentProperty> getPropertyPath() {
|
||||
return this.propertyPath;
|
||||
}
|
||||
|
||||
public DbAction.WithEntity<?> getDependingOn() {
|
||||
return this.dependingOn;
|
||||
}
|
||||
|
||||
public Map<PersistentPropertyPath<RelationalPersistentProperty>, 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<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a batch insert statement for a multiple entities that are not aggregate roots.
|
||||
*
|
||||
* @param <T> type of the entity for which this represents a database interaction.
|
||||
* @since 2.4
|
||||
*/
|
||||
final class InsertBatch<T> implements DbAction<T> {
|
||||
private final List<Insert<T>> inserts;
|
||||
private final IdValueSource idValueSource;
|
||||
|
||||
public InsertBatch(List<Insert<T>> inserts, IdValueSource idValueSource) {
|
||||
Assert.notEmpty(inserts, "Inserts must contains at least one insert");
|
||||
this.inserts = inserts;
|
||||
this.idValueSource = idValueSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> getEntityType() {
|
||||
return inserts.get(0).getEntityType();
|
||||
}
|
||||
|
||||
public List<Insert<T>> 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.
|
||||
*
|
||||
|
||||
@@ -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 <T> IdValueSource forInstance(Object instance, RelationalPersistentEntity<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<?, RelationalPersistentProperty> paths;
|
||||
private final Map<PathNode, DbAction<?>> previousActions = new HashMap<>();
|
||||
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<PathNode>> 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<DbAction<?>> insert() {
|
||||
|
||||
List<DbAction<?>> 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<DbAction<?>> 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<DbAction<?>> insertAll(PersistentPropertyPath<RelationalPersistentProperty> path) {
|
||||
|
||||
List<DbAction<?>> actions = new ArrayList<>();
|
||||
private List<? extends DbAction<?>> insertAll(PersistentPropertyPath<RelationalPersistentProperty> path) {
|
||||
|
||||
RelationalPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(path.getRequiredLeafProperty());
|
||||
List<DbAction.Insert<Object>> inserts = new ArrayList<>();
|
||||
from(path).forEach(node -> {
|
||||
|
||||
DbAction.WithEntity<?> parentAction = getAction(node.getParent());
|
||||
DbAction.Insert<Object> insert;
|
||||
Map<PersistentPropertyPath<RelationalPersistentProperty>, Object> qualifiers = new HashMap<>();
|
||||
Object instance;
|
||||
if (node.getPath().getRequiredLeafProperty().isQualified()) {
|
||||
|
||||
Pair<Object, Object> value = (Pair) node.getValue();
|
||||
Map<PersistentPropertyPath<RelationalPersistentProperty>, 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<Object> 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<DbAction.Insert<Object>> batch = entry.getValue();
|
||||
if (batch.size() > 1) {
|
||||
return new DbAction.InsertBatch<>(batch, entry.getKey());
|
||||
}
|
||||
return batch.get(0);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<DbAction<?>> 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)));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PrimitiveLongIdEntity> 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<PrimitiveIntIdEntity> 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<EntityWithReferencesToPrimitiveIdEntity> 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<DbAction<?>> 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<Insert<Element>> 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<DbAction<?>> 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<Insert<CascadingReferenceMiddleElement>> 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<Insert<Element>> 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<DbAction<?>> 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<Insert<CascadingReferenceMiddleElement>> 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<Insert<Element>> 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<DbAction<?>> 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<Insert<Element>> 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<DbAction<?>> 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<Insert<Element>> 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<DbAction<?>> 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<Insert<Element>> 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<ListContainer> aggregateChange = //
|
||||
new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, ListContainer.class, root);
|
||||
|
||||
converter.write(root, aggregateChange);
|
||||
|
||||
List<DbAction<?>> 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<Element> 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<Element> 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<EntityWithReferencesToPrimitiveIdEntity> aggregateChange = //
|
||||
new DefaultAggregateChange<>(AggregateChange.Kind.SAVE, EntityWithReferencesToPrimitiveIdEntity.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
List<DbAction<?>> 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 <T> InsertBatch<T> getInsertBatchAction(List<DbAction<?>> actions, Class<T> entityType) {
|
||||
return getInsertBatchActions(actions, entityType).stream()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("No InsertBatch action found!"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> InsertBatch<T> getInsertBatchAction(List<DbAction<?>> actions, Class<T> 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 <T> List<InsertBatch<T>> getInsertBatchActions(List<DbAction<?>> actions, Class<T> entityType) {
|
||||
//noinspection unchecked
|
||||
return actions.stream() //
|
||||
.filter(dbAction -> dbAction instanceof InsertBatch) //
|
||||
.filter(dbAction -> dbAction.getEntityType().equals(entityType)) //
|
||||
.map(dbAction -> (InsertBatch<T>) 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<PrimitiveLongIdEntity> primitiveLongIdEntities = new ArrayList<>();
|
||||
PrimitiveIntIdEntity primitiveIntIdEntity;
|
||||
List<PrimitiveIntIdEntity> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user