From 5b3fa3d9955b2752c75606180ac995ba6c5ab4e6 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Sun, 1 Jun 2025 16:20:15 +0300 Subject: [PATCH] Applies proper event handling before saving in batch. Signed-off-by: mipo256 Commit message edited by Jens Schauder. Original pull request #2065 Closes #2064 --- .../data/jdbc/core/JdbcAggregateTemplate.java | 87 +++++++++++-------- ...JdbcAggregateTemplateIntegrationTests.java | 58 ++++++++++++- ...cAggregateTemplateIntegrationTests-db2.sql | 10 ++- ...bcAggregateTemplateIntegrationTests-h2.sql | 8 +- ...AggregateTemplateIntegrationTests-hsql.sql | 8 +- ...regateTemplateIntegrationTests-mariadb.sql | 8 +- ...ggregateTemplateIntegrationTests-mssql.sql | 10 ++- ...ggregateTemplateIntegrationTests-mysql.sql | 8 +- ...gregateTemplateIntegrationTests-oracle.sql | 10 ++- ...egateTemplateIntegrationTests-postgres.sql | 10 ++- 10 files changed, 170 insertions(+), 47 deletions(-) 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 84076fa5d..edda1da4f 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 @@ -27,6 +27,7 @@ import java.util.function.Function; import java.util.stream.Collectors; 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; @@ -49,11 +50,22 @@ import org.springframework.data.relational.core.conversion.RootAggregateChange; 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; @@ -173,19 +185,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { @Override public List saveAll(Iterable instances) { - - 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, changeCreatorSelectorForSave(instance))); - } - return performSaveAll(entityAndChangeCreators); + return saveInBatch(instances, instance -> changeCreatorSelectorForSave(instance)); } /** @@ -206,21 +206,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { @Override public List insertAll(Iterable instances) { - - Assert.notNull(instances, "Aggregate instances must not be null"); - - if (!instances.iterator().hasNext()) { - return Collections.emptyList(); - } - - List> entityAndChangeCreators = new ArrayList<>(); - for (T instance : instances) { - - Function> changeCreator = entity -> createInsertChange(prepareVersionForInsert(entity)); - EntityAndChangeCreator entityChange = new EntityAndChangeCreator<>(instance, changeCreator); - entityAndChangeCreators.add(entityChange); - } - return performSaveAll(entityAndChangeCreators); + return doInBatch(instances, entity -> createInsertChange(prepareVersionForInsert(entity))); } /** @@ -241,6 +227,10 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { @Override public List updateAll(Iterable instances) { + return doInBatch(instances, entity -> createUpdateChange(prepareVersionForUpdate(entity))); + } + + private List saveInBatch(Iterable instances, Function> changes) { Assert.notNull(instances, "Aggregate instances must not be null"); @@ -249,11 +239,27 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { } 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"); - Function> changeCreator = entity -> createUpdateChange(prepareVersionForUpdate(entity)); - EntityAndChangeCreator entityChange = new EntityAndChangeCreator<>(instance, changeCreator); - entityAndChangeCreators.add(entityChange); + if (!instances.iterator().hasNext()) { + return Collections.emptyList(); + } + + List> entityAndChangeCreators = new ArrayList<>(); + for (T instance : instances) { + verifyIdProperty(instance); + entityAndChangeCreators.add(new EntityAndChangeCreator(instance, changeCreatorFunction)); } return performSaveAll(entityAndChangeCreators); } @@ -473,7 +479,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { T aggregateRoot = triggerBeforeConvert(instance.entity); - RootAggregateChange change = instance.changeCreator.apply(aggregateRoot); + RootAggregateChange change = instance.changeCreator.createAggregateChange(aggregateRoot); aggregateRoot = triggerBeforeSave(change.getRoot(), change); @@ -531,7 +537,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { return results; } - private Function> changeCreatorSelectorForSave(T instance) { + private AggregateChangeCreator changeCreatorSelectorForSave(T instance) { return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) ? entity -> createInsertChange(prepareVersionForInsert(entity)) @@ -671,6 +677,13 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { 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 c08e58de6..fffa53db8 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 @@ -28,9 +28,11 @@ import java.util.ArrayList; import java.util.function.Function; import java.util.stream.IntStream; +import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; 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.IntegrationTest; 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.conversion.DbActionExecutionException; import org.springframework.data.relational.core.mapping.Column; @@ -60,6 +63,7 @@ import org.springframework.data.relational.core.mapping.InsertOnlyProperty; 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; @@ -1328,6 +1332,22 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests { 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() { @@ -2139,6 +2159,32 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests { } } + @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 { @@ -2226,9 +2272,17 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests { } @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 e93990e31..a22367cd1 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 24ef5bdea..2d06a2141 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 21e80a6c9..824959d5d 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 14636eff4..43c43e869 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 d922614f2..e72cd53dc 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 3672630b2..87cb8734a 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 706e5e46d..636e68112 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 36f20896b..5c4a8c022 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 +);