Skip to content

Commit 60117f1

Browse files
committed
This reintroduces the behavior of BeforeSaveCallbacks to allow for creating an Id for an insert operation.
This behavior is strictly speaking not correct, since this kind of action should be performed in a BeforeConversionCallback. Closes #1232
1 parent 9b3aeef commit 60117f1

File tree

9 files changed

+154
-15
lines changed

9 files changed

+154
-15
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.data.mapping.IdentifierAccessor;
3232
import org.springframework.data.mapping.callback.EntityCallbacks;
3333
import org.springframework.data.relational.core.conversion.AggregateChange;
34+
import org.springframework.data.relational.core.conversion.DbAction;
35+
import org.springframework.data.relational.core.conversion.IdValueSource;
3436
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
3537
import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter;
3638
import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter;
@@ -339,9 +341,19 @@ private <T> T store(T aggregateRoot, Function<T, MutableAggregateChange<T>> chan
339341

340342
MutableAggregateChange<T> change = changeCreator.apply(aggregateRoot);
341343

342-
aggregateRoot = triggerBeforeSave(change.getEntity(), change);
344+
T newAggregateRoot = triggerBeforeSave(change.getEntity(), change);
343345

344-
change.setEntity(aggregateRoot);
346+
change.setEntity(newAggregateRoot);
347+
348+
change.forEachAction(a -> {
349+
350+
if (a instanceof DbAction.InsertRoot) {
351+
352+
IdValueSource idValueSource = IdValueSource.forInstance(newAggregateRoot, persistentEntity);
353+
((DbAction.InsertRoot<T>) a).updateAction(newAggregateRoot,
354+
idValueSource);
355+
}
356+
});
345357

346358
T entityAfterExecution = executor.execute(change);
347359

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.stream.IntStream;
4040

4141
import org.assertj.core.api.SoftAssertions;
42+
import org.junit.jupiter.api.BeforeEach;
4243
import org.junit.jupiter.api.Test;
4344
import org.junit.jupiter.api.extension.ExtendWith;
4445
import org.springframework.beans.factory.annotation.Autowired;
@@ -60,11 +61,15 @@
6061
import org.springframework.data.jdbc.testing.EnabledOnFeature;
6162
import org.springframework.data.jdbc.testing.TestConfiguration;
6263
import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
64+
import org.springframework.data.mapping.callback.EntityCallbacks;
6365
import org.springframework.data.relational.core.conversion.DbActionExecutionException;
66+
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
6467
import org.springframework.data.relational.core.mapping.Column;
6568
import org.springframework.data.relational.core.mapping.MappedCollection;
6669
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
6770
import org.springframework.data.relational.core.mapping.Table;
71+
import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
72+
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
6873
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
6974
import org.springframework.test.context.ContextConfiguration;
7075
import org.springframework.test.context.TestExecutionListeners;
@@ -93,9 +98,17 @@ class JdbcAggregateTemplateIntegrationTests {
9398

9499
@Autowired JdbcAggregateOperations template;
95100
@Autowired NamedParameterJdbcOperations jdbcTemplate;
101+
@Autowired Config.ConfigurableBeforeSaveCallback beforeSaveCallback;
102+
@Autowired Config.ConfigurableBeforeConvertCallback beforeConvertCallback;
96103

97104
LegoSet legoSet = createLegoSet("Star Destroyer");
98105

106+
@BeforeEach
107+
void beforeEach() {
108+
((JdbcAggregateTemplate) template)
109+
.setEntityCallbacks(EntityCallbacks.create(beforeSaveCallback, beforeConvertCallback));
110+
}
111+
99112
/**
100113
* creates an instance of {@link NoIdListChain4} with the following properties:
101114
* <ul>
@@ -258,14 +271,15 @@ void saveAndLoadManyEntitiesWithReferencedEntitySortedAndPaged() {
258271
}
259272

260273
@Test // GH-821
261-
@EnabledOnFeature({SUPPORTS_QUOTED_IDS, SUPPORTS_NULL_PRECEDENCE})
274+
@EnabledOnFeature({ SUPPORTS_QUOTED_IDS, SUPPORTS_NULL_PRECEDENCE })
262275
void saveAndLoadManyEntitiesWithReferencedEntitySortedWithNullPrecedence() {
263276

264277
template.save(createLegoSet(null));
265278
template.save(createLegoSet("Star"));
266279
template.save(createLegoSet("Frozen"));
267280

268-
Iterable<LegoSet> reloadedLegoSets = template.findAll(LegoSet.class, Sort.by(new Sort.Order(Sort.Direction.ASC, "name", Sort.NullHandling.NULLS_LAST)));
281+
Iterable<LegoSet> reloadedLegoSets = template.findAll(LegoSet.class,
282+
Sort.by(new Sort.Order(Sort.Direction.ASC, "name", Sort.NullHandling.NULLS_LAST)));
269283

270284
assertThat(reloadedLegoSets) //
271285
.extracting("name") //
@@ -843,7 +857,8 @@ void testUpdateEntityWithVersionDoesNotTriggerAnewConstructorInvocation() {
843857
assertThat(updatedRoot.version).isEqualTo(1L);
844858

845859
// Expect only one assignment of the version to AggregateWithImmutableVersion
846-
assertThat(AggregateWithImmutableVersion.constructorInvocations).containsOnly(new ConstructorInvocation(savedRoot.id, updatedRoot.version));
860+
assertThat(AggregateWithImmutableVersion.constructorInvocations)
861+
.containsOnly(new ConstructorInvocation(savedRoot.id, updatedRoot.version));
847862
}
848863

849864
@Test // DATAJDBC-219 Test that a delete with a version attribute works as expected.
@@ -956,6 +971,31 @@ void insertWithIdOnly() {
956971
assertThat(template.save(entity).id).isNotNull();
957972
}
958973

974+
@Test // GH-1232
975+
@EnabledOnFeature(IS_HSQL)
976+
void beforeSaveCallbackEffectsAreVisibleForInsert() {
977+
978+
beforeSaveCallback.behavior = m -> {
979+
980+
ManualId result = new ManualId();
981+
result.id = 23L;
982+
result.content = "changed by before safe";
983+
984+
return result;
985+
};
986+
987+
ManualId manual = new ManualId();
988+
manual.content = "original manual";
989+
990+
ManualId saved = template.save(manual);
991+
992+
assertThat(saved.content).isEqualTo("changed by before safe");
993+
994+
ManualId reloaded = template.findById(saved.id, ManualId.class);
995+
assertThat(reloaded.content).isEqualTo("changed by before safe");
996+
997+
}
998+
959999
private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate,
9601000
Function<Number, T> toConcreteNumber) {
9611001
saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0);
@@ -1373,6 +1413,11 @@ class WithIdOnly {
13731413
@Id Long id;
13741414
}
13751415

1416+
static class ManualId {
1417+
@Id private Long id;
1418+
private String content;
1419+
}
1420+
13761421
@Configuration
13771422
@Import(TestConfiguration.class)
13781423
static class Config {
@@ -1385,7 +1430,46 @@ Class<?> testClass() {
13851430
@Bean
13861431
JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context,
13871432
DataAccessStrategy dataAccessStrategy, JdbcConverter converter) {
1433+
13881434
return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy);
13891435
}
1436+
1437+
@Bean
1438+
ConfigurableBeforeSaveCallback beforeSaveCallback() {
1439+
return new ConfigurableBeforeSaveCallback();
1440+
}
1441+
1442+
@Bean
1443+
ConfigurableBeforeConvertCallback beforeConvertCallback() {
1444+
return new ConfigurableBeforeConvertCallback();
1445+
}
1446+
1447+
private static class ConfigurableBeforeSaveCallback implements BeforeSaveCallback<ManualId> {
1448+
1449+
Function<ManualId, ManualId> behavior;
1450+
1451+
@Override
1452+
public ManualId onBeforeSave(ManualId aggregate, MutableAggregateChange<ManualId> aggregateChange) {
1453+
1454+
if (behavior == null) {
1455+
return aggregate;
1456+
}
1457+
return behavior.apply(aggregate);
1458+
}
1459+
}
1460+
1461+
private static class ConfigurableBeforeConvertCallback implements BeforeConvertCallback<ManualId> {
1462+
1463+
Function<ManualId, ManualId> behavior;
1464+
1465+
@Override
1466+
public ManualId onBeforeConvert(ManualId aggregate) {
1467+
1468+
if (behavior == null) {
1469+
return aggregate;
1470+
}
1471+
return behavior.apply(aggregate);
1472+
}
1473+
}
13901474
}
13911475
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ public void manuallyGeneratedId() {
110110

111111
assertThat(saved.getId()).isNotNull();
112112

113-
assertThat(immutableWithManualIdEntityRepository.findAll()).hasSize(1);
113+
final Iterable<ImmutableWithManualIdEntity> reloaded = immutableWithManualIdEntityRepository.findAll();
114+
assertThat(reloaded).extracting(i -> i.id).containsExactly(TestConfiguration.lastId.get());
114115
}
115116

116117
private interface PrimitiveIdEntityRepository extends CrudRepository<PrimitiveIdEntity, Long> {}
@@ -147,7 +148,7 @@ static class ImmutableWithManualIdEntity {
147148
includeFilters = @ComponentScan.Filter(value = CrudRepository.class, type = FilterType.ASSIGNABLE_TYPE))
148149
static class TestConfiguration {
149150

150-
AtomicLong lastId = new AtomicLong(0);
151+
static AtomicLong lastId = new AtomicLong(0);
151152

152153
@Bean
153154
Class<?> testClass() {

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,10 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
328328
CREATE TABLE WITH_ID_ONLY
329329
(
330330
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY
331-
)
331+
);
332+
333+
CREATE TABLE MANUAL_ID
334+
(
335+
ID BIGINT PRIMARY KEY,
336+
CONTENT VARCHAR(2000)
337+
);

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ public String toString() {
106106
*/
107107
class InsertRoot<T> implements WithGeneratedId<T> {
108108

109-
private final T entity;
110-
private final IdValueSource idValueSource;
109+
private T entity;
110+
private IdValueSource idValueSource;
111111

112112
public InsertRoot(T entity, IdValueSource idValueSource) {
113113

@@ -127,6 +127,21 @@ public IdValueSource getIdValueSource() {
127127
public String toString() {
128128
return "InsertRoot{" + "entity=" + entity + ", idValueSource=" + idValueSource + '}';
129129
}
130+
131+
/**
132+
* Updates the entity and {@link IdValueSource} of this action.
133+
*
134+
* @param newAggregateRoot must not be {@literal null}.
135+
* @param idValueSource must not be {@literal null}.
136+
* @deprecated This method exists only to support a temporary workaround. It will be removed completely without
137+
* replacement.
138+
*/
139+
@Deprecated
140+
public void updateAction(T newAggregateRoot, IdValueSource idValueSource) {
141+
142+
this.entity = newAggregateRoot;
143+
this.idValueSource = idValueSource;
144+
}
130145
}
131146

132147
/**

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeConvertCallback.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
/**
2121
* An {@link EntityCallback} that gets invoked before the aggregate is converted into a database change. The decision if
22-
* the change will be an insert or update is made after this callback gets called.
22+
* the change will be an insert or update is already made when this callback gets called.
23+
* <p>
24+
* The main use of this kind of callback is to create application side Ids for aggregates where this isn't done on the
25+
* database side.
2326
*
2427
* @author Jens Schauder
2528
* @author Mark Paluch

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeSaveCallback.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
/**
2222
* An {@link EntityCallback} that gets invoked before changes are applied to the database, after the aggregate was
2323
* converted to a database change.
24+
* <p>
25+
* Changes to the aggregate are not taken into account to decide whether the change will be an insert or update. Use the
26+
* {@link BeforeConvertCallback} to change the persistent the entity before being converted.
27+
* <p>
28+
* Currently a {@link BeforeSaveCallback} may be used to create an Id for new aggregate roots. This is for backward
29+
* compatibility purposes within the 2.x development line. The preferred way to do this is to use a
30+
* {@link BeforeConvertCallback} or {@link BeforeConvertEvent} and beginning with 3.0. this will be the only correct way
31+
* to do this.
2432
*
2533
* @author Jens Schauder
2634
* @author Mark Paluch
@@ -32,9 +40,7 @@ public interface BeforeSaveCallback<T> extends EntityCallback<T> {
3240
/**
3341
* Entity callback method invoked before an aggregate root is saved. Can return either the same or a modified instance
3442
* of the aggregate and can modify {@link MutableAggregateChange} contents. This method is called after converting the
35-
* {@code aggregate} to {@link MutableAggregateChange}. Changes to the aggregate are not taken into account to decide
36-
* whether the change will be an insert or update. Use the {@link BeforeConvertCallback} to change the persistent the
37-
* entity before being converted.
43+
* {@code aggregate} to {@link MutableAggregateChange}.
3844
*
3945
* @param aggregate the aggregate.
4046
* @param aggregateChange the associated {@link MutableAggregateChange}.

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/event/BeforeSaveEvent.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,20 @@
1515
*/
1616
package org.springframework.data.relational.core.mapping.event;
1717

18+
import org.springframework.context.ApplicationEvent;
1819
import org.springframework.data.relational.core.conversion.AggregateChange;
1920

2021
/**
21-
* Gets published before an entity gets saved to the database.
22+
* An {@link ApplicationEvent} that gets triggered before changes are applied to the database, after the aggregate was
23+
* converted to a database change.
24+
* <p>
25+
* Changes to the aggregate are not taken into account to decide whether the change will be an insert or update. Use the
26+
* {@link BeforeConvertCallback} to change the persistent the entity before being converted.
27+
* <p>
28+
* Currently a {@link BeforeSaveEvent} may be used to create an Id for new aggregate roots. This is for backward
29+
* compatibility purposes within the 2.x development line. The preferred way to do this is to use a
30+
* {@link BeforeConvertCallback} or {@link BeforeConvertEvent} and beginning with 3.0. this will be the only correct way
31+
* to do this.
2232
*
2333
* @author Jens Schauder
2434
*/

src/main/asciidoc/jdbc.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,8 @@ This is the correct callback if you want to set an id programmatically.
902902
| {javadoc-base}/org/springframework/data/relational/core/mapping/event/BeforeSaveCallback.html[`BeforeSaveCallback`]
903903
| Before an aggregate root gets saved (that is, inserted or updated but after the decision about whether if it gets inserted or updated was made).
904904

905+
In the 2.x version of Spring Data JDBC you may use it to create ids for entities. The preferred way an in the future only correct callback to use is `BeforeConvertCallback`.
906+
905907
| {javadoc-base}org/springframework/data/relational/core/mapping/event/AfterSaveCallback.html[`AfterSaveCallback`]
906908
| After an aggregate root gets saved (that is, inserted or updated).
907909

0 commit comments

Comments
 (0)