diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 928a18fcd6..4ff357d317 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -28,6 +28,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -50,11 +51,22 @@ 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.mapping.event.*; +import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent; +import org.springframework.data.relational.core.mapping.event.AfterConvertCallback; +import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; +import org.springframework.data.relational.core.mapping.event.AfterDeleteCallback; +import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent; +import org.springframework.data.relational.core.mapping.event.AfterSaveCallback; +import org.springframework.data.relational.core.mapping.event.AfterSaveEvent; +import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.relational.core.mapping.event.BeforeConvertEvent; +import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback; +import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent; +import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; +import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.relational.core.mapping.event.Identifier; import org.springframework.data.relational.core.query.Query; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -175,7 +187,7 @@ public T save(T instance) { @Override public List saveAll(Iterable instances) { - return doInBatch(instances, (first) -> (second -> changeCreatorSelectorForSave(first).apply(second))); + return saveInBatch(instances, instance -> changeCreatorSelectorForSave(instance)); } /** @@ -196,7 +208,7 @@ public T insert(T instance) { @Override public List insertAll(Iterable instances) { - return doInBatch(instances, (__) -> (entity -> createInsertChange(prepareVersionForInsert(entity)))); + return doInBatch(instances, entity -> createInsertChange(prepareVersionForInsert(entity))); } /** @@ -217,10 +229,28 @@ public T update(T instance) { @Override public List updateAll(Iterable instances) { - return doInBatch(instances, (__) -> (entity -> createUpdateChange(prepareVersionForUpdate(entity)))); + return doInBatch(instances, entity -> createUpdateChange(prepareVersionForUpdate(entity))); } - private List doInBatch(Iterable instances,Function>> changeCreatorFunction) { + private List saveInBatch(Iterable instances, Function> changes) { + + Assert.notNull(instances, "Aggregate instances must not be null"); + + if (!instances.iterator().hasNext()) { + return Collections.emptyList(); + } + + List> entityAndChangeCreators = new ArrayList<>(); + + for (T instance : instances) { + verifyIdProperty(instance); + entityAndChangeCreators.add(new EntityAndChangeCreator<>(instance, changes.apply(instance))); + } + + return performSaveAll(entityAndChangeCreators); + } + + private List doInBatch(Iterable instances, AggregateChangeCreator changeCreatorFunction) { Assert.notNull(instances, "Aggregate instances must not be null"); @@ -231,7 +261,7 @@ private List doInBatch(Iterable instances,Function> entityAndChangeCreators = new ArrayList<>(); for (T instance : instances) { verifyIdProperty(instance); - entityAndChangeCreators.add(new EntityAndChangeCreator(instance, changeCreatorFunction.apply(instance))); + entityAndChangeCreators.add(new EntityAndChangeCreator(instance, changeCreatorFunction)); } return performSaveAll(entityAndChangeCreators); } @@ -484,7 +514,7 @@ private RootAggregateChange beforeExecute(EntityAndChangeCreator insta T aggregateRoot = triggerBeforeConvert(instance.entity); - RootAggregateChange change = instance.changeCreator.apply(aggregateRoot); + RootAggregateChange change = instance.changeCreator.createAggregateChange(aggregateRoot); aggregateRoot = triggerBeforeSave(change.getRoot(), change); @@ -542,7 +572,7 @@ private List performSaveAll(Iterable> instances return results; } - private Function> changeCreatorSelectorForSave(T instance) { + private AggregateChangeCreator changeCreatorSelectorForSave(T instance) { return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) ? entity -> createInsertChange(prepareVersionForInsert(entity)) @@ -681,6 +711,13 @@ private T triggerBeforeDelete(@Nullable T aggregateRoot, Object id, MutableA private record EntityAndPreviousVersion (T entity, @Nullable Number version) { } - private record EntityAndChangeCreator (T entity, Function> changeCreator) { + private record EntityAndChangeCreator (T entity, AggregateChangeCreator changeCreator) { + } + + private interface AggregateChangeCreator extends Function> { + + default RootAggregateChange createAggregateChange(T instance) { + return this.apply(instance); + } } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index 00ce1278c7..c3753a5bbb 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -29,8 +29,10 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -52,6 +54,7 @@ import org.springframework.data.jdbc.testing.TestClass; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestDatabaseFeatures; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; @@ -59,6 +62,7 @@ import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.CriteriaDefinition; import org.springframework.data.relational.core.query.Query; @@ -1371,6 +1375,22 @@ void mapWithEnumKey() { assertThat(enumMapOwners).containsExactly(enumMapOwner); } + @Test //GH-2064 + void saveAllBeforeConvertCallback() { + var first = new BeforeConvertCallbackForSaveBatch("first"); + var second = new BeforeConvertCallbackForSaveBatch("second"); + var third = new BeforeConvertCallbackForSaveBatch("third"); + + template.saveAll(List.of(first, second, third)); + + var allEntriesInTable = template.findAll(BeforeConvertCallbackForSaveBatch.class); + + Assertions.assertThat(allEntriesInTable) + .hasSize(3) + .extracting(BeforeConvertCallbackForSaveBatch::getName) + .containsOnly("first", "second", "third"); + } + @Test // GH-1684 void oneToOneWithIdenticalIdColumnName() { @@ -2182,6 +2202,32 @@ public Short getVersion() { } } + @Table("BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH") + static class BeforeConvertCallbackForSaveBatch { + + @Id + private String id; + + private String name; + + public BeforeConvertCallbackForSaveBatch(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public BeforeConvertCallbackForSaveBatch setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + } + @Table("VERSIONED_AGGREGATE") static class AggregateWithPrimitiveShortVersion extends VersionedAggregate { @@ -2269,9 +2315,17 @@ TestClass testClass() { } @Bean - JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, + BeforeConvertCallback callback() { + return aggregate -> { + aggregate.setId(UUID.randomUUID().toString()); + return aggregate; + }; + } + + @Bean + JdbcAggregateOperations operations(ApplicationContext applicationContext, RelationalMappingContext context, DataAccessStrategy dataAccessStrategy, JdbcConverter converter) { - return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + return new JdbcAggregateTemplate(applicationContext, context, converter, dataAccessStrategy); } } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql index e93990e31b..a22367cd1a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql @@ -59,6 +59,8 @@ DROP TABLE THIRD; DROP TABLE SEC; DROP TABLE FIRST; +DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH; + CREATE TABLE LEGO_SET ( "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, @@ -467,4 +469,10 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR(36) PRIMARY KEY NOT NULL, + NAME VARCHAR(20) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index 24ef5bdeab..2d06a21416 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -417,4 +417,10 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR PRIMARY KEY, + NAME VARCHAR +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index 21e80a6c98..824959d5d7 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -419,4 +419,10 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR PRIMARY KEY, + NAME VARCHAR +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 14636eff40..43c43e8697 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -391,4 +391,10 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR(36) PRIMARY KEY, + NAME VARCHAR(20) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index d922614f26..e72cd53dc3 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -441,4 +441,12 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH; + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR PRIMARY KEY, + NAME VARCHAR +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 3672630b26..87cb8734a3 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -397,4 +397,10 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR(36) PRIMARY KEY, + NAME VARCHAR(20) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql index 706e5e46d9..636e68112a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql @@ -49,6 +49,8 @@ DROP TABLE THIRD CASCADE CONSTRAINTS PURGE; DROP TABLE SEC CASCADE CONSTRAINTS PURGE; DROP TABLE FIRST CASCADE CONSTRAINTS PURGE; +DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH CASCADE CONSTRAINTS PURGE; + CREATE TABLE LEGO_SET ( "id1" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, @@ -447,4 +449,10 @@ CREATE TABLE THIRD SEC NUMBER NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH +( + ID VARCHAR PRIMARY KEY, + NAME VARCHAR +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index 36f20896b7..5c4a8c0225 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -52,6 +52,8 @@ DROP TABLE THIRD; DROP TABLE SEC; DROP TABLE FIRST; +DROP TABLE "BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH"; + CREATE TABLE LEGO_SET ( "id1" SERIAL PRIMARY KEY, @@ -470,4 +472,10 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE "BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH" +( + ID VARCHAR PRIMARY KEY, + NAME VARCHAR +);