Applies proper event handling before saving in batch.

Signed-off-by: mipo256 <mikhailpolivakha@gmail.com>

Commit message edited by Jens Schauder.

Original pull request #2065
Closes #2064
This commit is contained in:
mipo256
2025-06-01 16:20:15 +03:00
committed by Jens Schauder
parent 1e50d5aaca
commit 5b3fa3d995
10 changed files with 171 additions and 48 deletions

View File

@@ -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 <T> List<T> saveAll(Iterable<T> instances) {
Assert.notNull(instances, "Aggregate instances must not be null");
if (!instances.iterator().hasNext()) {
return Collections.emptyList();
}
List<EntityAndChangeCreator<T>> 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 <T> List<T> insertAll(Iterable<T> instances) {
Assert.notNull(instances, "Aggregate instances must not be null");
if (!instances.iterator().hasNext()) {
return Collections.emptyList();
}
List<EntityAndChangeCreator<T>> entityAndChangeCreators = new ArrayList<>();
for (T instance : instances) {
Function<T, RootAggregateChange<T>> changeCreator = entity -> createInsertChange(prepareVersionForInsert(entity));
EntityAndChangeCreator<T> entityChange = new EntityAndChangeCreator<>(instance, changeCreator);
entityAndChangeCreators.add(entityChange);
}
return performSaveAll(entityAndChangeCreators);
return doInBatch(instances, entity -> createInsertChange(prepareVersionForInsert(entity)));
}
/**
@@ -241,6 +227,28 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
@Override
public <T> List<T> updateAll(Iterable<T> instances) {
return doInBatch(instances, entity -> createUpdateChange(prepareVersionForUpdate(entity)));
}
private <T> List<T> saveInBatch(Iterable<T> instances, Function<T, AggregateChangeCreator<T>> changes) {
Assert.notNull(instances, "Aggregate instances must not be null");
if (!instances.iterator().hasNext()) {
return Collections.emptyList();
}
List<EntityAndChangeCreator<T>> entityAndChangeCreators = new ArrayList<>();
for (T instance : instances) {
verifyIdProperty(instance);
entityAndChangeCreators.add(new EntityAndChangeCreator<>(instance, changes.apply(instance)));
}
return performSaveAll(entityAndChangeCreators);
}
private <T> List<T> doInBatch(Iterable<T> instances, AggregateChangeCreator<T> changeCreatorFunction) {
Assert.notNull(instances, "Aggregate instances must not be null");
@@ -250,10 +258,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
List<EntityAndChangeCreator<T>> entityAndChangeCreators = new ArrayList<>();
for (T instance : instances) {
Function<T, RootAggregateChange<T>> changeCreator = entity -> createUpdateChange(prepareVersionForUpdate(entity));
EntityAndChangeCreator<T> entityChange = new EntityAndChangeCreator<>(instance, changeCreator);
entityAndChangeCreators.add(entityChange);
verifyIdProperty(instance);
entityAndChangeCreators.add(new EntityAndChangeCreator<T>(instance, changeCreatorFunction));
}
return performSaveAll(entityAndChangeCreators);
}
@@ -473,7 +479,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
T aggregateRoot = triggerBeforeConvert(instance.entity);
RootAggregateChange<T> change = instance.changeCreator.apply(aggregateRoot);
RootAggregateChange<T> change = instance.changeCreator.createAggregateChange(aggregateRoot);
aggregateRoot = triggerBeforeSave(change.getRoot(), change);
@@ -531,7 +537,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
return results;
}
private <T> Function<T, RootAggregateChange<T>> changeCreatorSelectorForSave(T instance) {
private <T> AggregateChangeCreator<T> 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> (T entity, @Nullable Number version) {
}
private record EntityAndChangeCreator<T> (T entity, Function<T, RootAggregateChange<T>> changeCreator) {
private record EntityAndChangeCreator<T> (T entity, AggregateChangeCreator<T> changeCreator) {
}
private interface AggregateChangeCreator<T> extends Function<T, RootAggregateChange<T>> {
default RootAggregateChange<T> createAggregateChange(T instance) {
return this.apply(instance);
}
}
}

View File

@@ -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<BeforeConvertCallbackForSaveBatch> 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);
}
}

View File

@@ -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)
);
);
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR(36) PRIMARY KEY NOT NULL,
NAME VARCHAR(20)
);

View File

@@ -417,4 +417,10 @@ CREATE TABLE THIRD
SEC BIGINT NOT NULL,
NAME VARCHAR(20) NOT NULL,
FOREIGN KEY (SEC) REFERENCES SEC (ID)
);
);
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR PRIMARY KEY,
NAME VARCHAR
);

View File

@@ -419,4 +419,10 @@ CREATE TABLE THIRD
SEC BIGINT NOT NULL,
NAME VARCHAR(20) NOT NULL,
FOREIGN KEY (SEC) REFERENCES SEC (ID)
);
);
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR PRIMARY KEY,
NAME VARCHAR
);

View File

@@ -391,4 +391,10 @@ CREATE TABLE THIRD
SEC BIGINT NOT NULL,
NAME VARCHAR(20) NOT NULL,
FOREIGN KEY (SEC) REFERENCES SEC (ID)
);
);
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR(36) PRIMARY KEY,
NAME VARCHAR(20)
);

View File

@@ -441,4 +441,12 @@ CREATE TABLE THIRD
SEC BIGINT NOT NULL,
NAME VARCHAR(20) NOT NULL,
FOREIGN KEY (SEC) REFERENCES SEC (ID)
);
);
DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH;
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR PRIMARY KEY,
NAME VARCHAR
);

View File

@@ -397,4 +397,10 @@ CREATE TABLE THIRD
SEC BIGINT NOT NULL,
NAME VARCHAR(20) NOT NULL,
FOREIGN KEY (SEC) REFERENCES SEC (ID)
);
);
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR(36) PRIMARY KEY,
NAME VARCHAR(20)
);

View File

@@ -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)
);
);
CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
(
ID VARCHAR PRIMARY KEY,
NAME VARCHAR
);

View File

@@ -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)
);
);
CREATE TABLE "BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH"
(
ID VARCHAR PRIMARY KEY,
NAME VARCHAR
);