#451 - Initialize version for versioned entities after BeforeConvert callback.

We now initialize/increment the version of versioned entities (optimistic locking) after running the BeforeConvert callback. BeforeConvert callbacks may contain features such as auditing and if the auditing callback issues a isNew check, then the version would be already populated which leads to the auditing callback considering the entity not new anymore leaving the created date empty.
This commit is contained in:
Mark Paluch
2020-09-09 09:25:21 +02:00
parent f0732a5388
commit 43d5d9d827
2 changed files with 95 additions and 16 deletions

View File

@@ -511,13 +511,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
RelationalPersistentEntity<T> persistentEntity = getRequiredEntity(entity);
T entityWithVersion = setVersionIfNecessary(persistentEntity, entity);
return maybeCallBeforeConvert(entity, tableName).flatMap(it -> {
return maybeCallBeforeConvert(entityWithVersion, tableName).flatMap(beforeConvert -> {
T initializedEntity = setVersionIfNecessary(persistentEntity, it);
OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(beforeConvert);
OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(initializedEntity);
return maybeCallBeforeSave(beforeConvert, outboundRow, tableName) //
return maybeCallBeforeSave(initializedEntity, outboundRow, tableName) //
.flatMap(entityToSave -> doInsert(entityToSave, tableName, outboundRow));
});
}
@@ -577,24 +577,24 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
RelationalPersistentEntity<T> persistentEntity = getRequiredEntity(entity);
T entityToUse;
Criteria matchingVersionCriteria;
return maybeCallBeforeConvert(entity, tableName).flatMap(it -> {
if (persistentEntity.hasVersionProperty()) {
T entityToUse;
Criteria matchingVersionCriteria;
matchingVersionCriteria = createMatchingVersionCriteria(entity, persistentEntity);
entityToUse = incrementVersion(persistentEntity, entity);
} else {
if (persistentEntity.hasVersionProperty()) {
entityToUse = entity;
matchingVersionCriteria = null;
}
matchingVersionCriteria = createMatchingVersionCriteria(it, persistentEntity);
entityToUse = incrementVersion(persistentEntity, it);
} else {
return maybeCallBeforeConvert(entityToUse, tableName).flatMap(beforeConvert -> {
entityToUse = entity;
matchingVersionCriteria = null;
}
OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(beforeConvert);
OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(entityToUse);
return maybeCallBeforeSave(beforeConvert, outboundRow, tableName) //
return maybeCallBeforeSave(entityToUse, outboundRow, tableName) //
.flatMap(entityToSave -> {
SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName();

View File

@@ -16,6 +16,7 @@
package org.springframework.data.r2dbc.core;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.test.MockColumnMetadata;
import io.r2dbc.spi.test.MockResult;
@@ -27,6 +28,7 @@ import lombok.With;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -34,16 +36,22 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.mapping.OutboundRow;
import org.springframework.data.r2dbc.mapping.event.AfterConvertCallback;
import org.springframework.data.r2dbc.mapping.event.AfterSaveCallback;
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
import org.springframework.data.r2dbc.mapping.event.ReactiveAuditingEntityCallback;
import org.springframework.data.r2dbc.testing.StatementRecorder;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.query.Criteria;
@@ -254,6 +262,63 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from(1L));
}
@Test // gh-451
public void shouldInsertCorrectlyVersionedAndAudited() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build();
recorder.addStubbing(s -> s.startsWith("INSERT"), result);
ObjectFactory<ReactiveIsNewAwareAuditingHandler> objectFactory = mock(ObjectFactory.class);
when(objectFactory.getObject()).thenReturn(new ReactiveIsNewAwareAuditingHandler(
PersistentEntities.of(entityTemplate.getConverter().getMappingContext())));
entityTemplate
.setEntityCallbacks(ReactiveEntityCallbacks.create(new ReactiveAuditingEntityCallback(objectFactory)));
entityTemplate.insert(new WithAuditingAndOptimisticLocking(null, 0, "Walter", null, null)) //
.as(StepVerifier::create) //
.assertNext(actual -> {
assertThat(actual.getVersion()).isEqualTo(1);
assertThat(actual.getCreatedDate()).isNotNull();
}) //
.verifyComplete();
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT"));
assertThat(statement.getSql()).isEqualTo(
"INSERT INTO with_auditing_and_optimistic_locking (version, name, created_date, last_modified_date) VALUES ($1, $2, $3, $4)");
}
@Test // gh-451
public void shouldUpdateCorrectlyVersionedAndAudited() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build();
recorder.addStubbing(s -> s.startsWith("UPDATE"), result);
ObjectFactory<ReactiveIsNewAwareAuditingHandler> objectFactory = mock(ObjectFactory.class);
when(objectFactory.getObject()).thenReturn(new ReactiveIsNewAwareAuditingHandler(
PersistentEntities.of(entityTemplate.getConverter().getMappingContext())));
entityTemplate
.setEntityCallbacks(ReactiveEntityCallbacks.create(new ReactiveAuditingEntityCallback(objectFactory)));
entityTemplate.update(new WithAuditingAndOptimisticLocking(null, 2, "Walter", null, null)) //
.as(StepVerifier::create) //
.assertNext(actual -> {
assertThat(actual.getVersion()).isEqualTo(3);
assertThat(actual.getCreatedDate()).isNull();
assertThat(actual.getLastModifiedDate()).isNotNull();
}) //
.verifyComplete();
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE"));
assertThat(statement.getSql()).startsWith(
"UPDATE with_auditing_and_optimistic_locking SET version = $1, name = $2, created_date = $3, last_modified_date = $4");
}
@Test // gh-215
public void insertShouldInvokeCallback() {
@@ -366,6 +431,20 @@ public class R2dbcEntityTemplateUnitTests {
String name;
}
@Value
@With
static class WithAuditingAndOptimisticLocking {
@Id String id;
@Version long version;
String name;
@CreatedDate LocalDateTime createdDate;
@LastModifiedDate LocalDateTime lastModifiedDate;
}
static class ValueCapturingEntityCallback<T> {
private final List<T> values = new ArrayList<>(1);