DATAMONGO-2346 - Fix (reactive)auditing of immutable versioned entities.
We now check if the source is still the same object after potentially applying auditing modifications and make sure to pass the audited object on to the mapping layer. Limited bean inspection scope of event listener tests to avoid side effects in index creation. Original pull request: #780.
This commit is contained in:
committed by
Mark Paluch
parent
e9a2b84af5
commit
b2e3e3fb8e
@@ -1430,6 +1430,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
|
||||
toSave = maybeEmitEvent(new BeforeConvertEvent<T>(toSave, collectionName)).getSource();
|
||||
toSave = maybeCallBeforeConvert(toSave, collectionName);
|
||||
|
||||
if (source.getBean() != toSave) {
|
||||
source = operations.forEntity(toSave, mongoConverter.getConversionService());
|
||||
}
|
||||
|
||||
source.assertUpdateableIdIfNotSet();
|
||||
|
||||
MappedDocument mapped = source.toMappedDocument(mongoConverter);
|
||||
|
||||
@@ -1542,7 +1542,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
|
||||
|
||||
return maybeCallBeforeConvert(afterEvent, collectionName).flatMap(toConvert -> {
|
||||
|
||||
MappedDocument mapped = operations.forEntity(toSave).toMappedDocument(mongoConverter);
|
||||
MappedDocument mapped = operations.forEntity(toConvert).toMappedDocument(mongoConverter);
|
||||
Document document = mapped.getDocument();
|
||||
|
||||
maybeEmitEvent(new BeforeSaveEvent<>(toConvert, document, collectionName));
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.auditing;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
|
||||
import org.springframework.data.mongodb.config.EnableMongoAuditing;
|
||||
import org.springframework.data.mongodb.core.KAuditableVersionedEntity;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.mongodb.MongoClient;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class MongoTemplateAuditingTests {
|
||||
|
||||
@Configuration
|
||||
@EnableMongoAuditing
|
||||
static class Conf extends AbstractMongoConfiguration {
|
||||
|
||||
@Override
|
||||
public MongoClient mongoClient() {
|
||||
return new MongoClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDatabaseName() {
|
||||
return "mongo-template-audit-tests";
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired MongoTemplate template;
|
||||
|
||||
@Test // DATAMONGO-2346
|
||||
public void auditingSetsLastModifiedDateCorrectlyForImmutableVersionedEntityOnSave() throws InterruptedException {
|
||||
|
||||
template.remove(new Query(), ImmutableAuditableEntityWithVersion.class);
|
||||
|
||||
ImmutableAuditableEntityWithVersion entity = new ImmutableAuditableEntityWithVersion("id-1", "value", null, null);
|
||||
ImmutableAuditableEntityWithVersion inserted = template.save(entity);
|
||||
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
|
||||
ImmutableAuditableEntityWithVersion modified = inserted.withValue("changed-value");
|
||||
ImmutableAuditableEntityWithVersion updated = template.save(modified);
|
||||
|
||||
ImmutableAuditableEntityWithVersion fetched = template.findOne(Query.query(Criteria.where("id").is(entity.id)),
|
||||
ImmutableAuditableEntityWithVersion.class);
|
||||
|
||||
assertThat(updated.modificationDate).isAfter(inserted.modificationDate);
|
||||
assertThat(fetched.modificationDate).isAfter(inserted.modificationDate);
|
||||
assertThat(fetched.modificationDate).isEqualTo(updated.modificationDate);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2346
|
||||
public void auditingSetsLastModifiedDateCorrectlyForImmutableVersionedKotlinEntityOnSave()
|
||||
throws InterruptedException {
|
||||
|
||||
template.remove(new Query(), KAuditableVersionedEntity.class);
|
||||
|
||||
KAuditableVersionedEntity entity = new KAuditableVersionedEntity("kId-1", "value", null, null);
|
||||
KAuditableVersionedEntity inserted = template.save(entity);
|
||||
|
||||
TimeUnit.MILLISECONDS.sleep(500);
|
||||
|
||||
KAuditableVersionedEntity updated = template.save(inserted.withValue("changed-value"));
|
||||
|
||||
KAuditableVersionedEntity fetched = template.findOne(Query.query(Criteria.where("id").is(entity.getId())),
|
||||
KAuditableVersionedEntity.class);
|
||||
|
||||
assertThat(updated.getModificationDate()).isAfter(inserted.getModificationDate());
|
||||
assertThat(fetched.getModificationDate()).isAfter(inserted.getModificationDate());
|
||||
assertThat(fetched.getModificationDate()).isEqualTo(updated.getModificationDate());
|
||||
}
|
||||
|
||||
static class ImmutableAuditableEntityWithVersion {
|
||||
|
||||
final @Id String id;
|
||||
final String value;
|
||||
final @Version Integer version;
|
||||
final @LastModifiedDate Instant modificationDate;
|
||||
|
||||
ImmutableAuditableEntityWithVersion(String id, String value, Integer version, Instant modificationDate) {
|
||||
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
this.version = version;
|
||||
this.modificationDate = modificationDate;
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withValue(String value) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withModificationDate(Instant modificationDate) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withVersion(Integer version) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.auditing;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
|
||||
import org.springframework.data.mongodb.config.EnableMongoAuditing;
|
||||
import org.springframework.data.mongodb.core.KAuditableVersionedEntity;
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestUtils;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.mongodb.reactivestreams.client.MongoClient;
|
||||
import com.mongodb.reactivestreams.client.MongoClients;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class ReactiveMongoTemplateAuditingTests {
|
||||
|
||||
static final String DB_NAME = "mongo-template-audit-tests";
|
||||
|
||||
@Configuration
|
||||
@EnableMongoAuditing
|
||||
static class Conf extends AbstractReactiveMongoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
public MongoClient reactiveMongoClient() {
|
||||
return MongoClients.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDatabaseName() {
|
||||
return DB_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired ReactiveMongoTemplate template;
|
||||
@Autowired MongoClient client;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
MongoTestUtils.flushCollection(DB_NAME, template.getCollectionName(ImmutableAuditableEntityWithVersion.class),
|
||||
client);
|
||||
MongoTestUtils.flushCollection(DB_NAME, template.getCollectionName(KAuditableVersionedEntity.class), client);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2346
|
||||
public void auditingSetsLastModifiedDateCorrectlyForImmutableVersionedEntityOnSave() {
|
||||
|
||||
ImmutableAuditableEntityWithVersion entity = new ImmutableAuditableEntityWithVersion(null, "value", null, null);
|
||||
|
||||
template.save(entity).delayElement(Duration.ofMillis(500)) //
|
||||
.flatMap(inserted -> template.save(inserted.withValue("changed-value")) //
|
||||
.map(updated -> Tuples.of(inserted, updated))) //
|
||||
.flatMap(tuple2 -> template
|
||||
.findOne(Query.query(Criteria.where("id").is(tuple2.getT1().id)), ImmutableAuditableEntityWithVersion.class)
|
||||
.map(fetched -> Tuples.of(tuple2.getT1(), tuple2.getT2(), fetched))) //
|
||||
.as(StepVerifier::create) //
|
||||
.consumeNextWith(tuple3 -> {
|
||||
|
||||
assertThat(tuple3.getT2().modificationDate).isAfter(tuple3.getT1().modificationDate);
|
||||
assertThat(tuple3.getT3().modificationDate).isAfter(tuple3.getT1().modificationDate);
|
||||
assertThat(tuple3.getT3().modificationDate).isEqualTo(tuple3.getT2().modificationDate);
|
||||
}) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2346
|
||||
public void auditingSetsLastModifiedDateCorrectlyForImmutableVersionedKotlinEntityOnSave() {
|
||||
|
||||
KAuditableVersionedEntity entity = new KAuditableVersionedEntity(null, "value", null, null);
|
||||
|
||||
template.save(entity).delayElement(Duration.ofMillis(500)) //
|
||||
.flatMap(inserted -> template.save(inserted.withValue("changed-value")) //
|
||||
.map(updated -> Tuples.of(inserted, updated))) //
|
||||
.flatMap(tuple2 -> template
|
||||
.findOne(Query.query(Criteria.where("id").is(tuple2.getT1().getId())), KAuditableVersionedEntity.class)
|
||||
.map(fetched -> Tuples.of(tuple2.getT1(), tuple2.getT2(), fetched))) //
|
||||
.as(StepVerifier::create) //
|
||||
.consumeNextWith(tuple3 -> {
|
||||
|
||||
assertThat(tuple3.getT2().getModificationDate()).isAfter(tuple3.getT1().getModificationDate());
|
||||
assertThat(tuple3.getT3().getModificationDate()).isAfter(tuple3.getT1().getModificationDate());
|
||||
assertThat(tuple3.getT3().getModificationDate()).isEqualTo(tuple3.getT2().getModificationDate());
|
||||
}) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Document("versioned-auditable")
|
||||
static class ImmutableAuditableEntityWithVersion {
|
||||
|
||||
final @Id String id;
|
||||
final String value;
|
||||
final @Version Integer version;
|
||||
final @LastModifiedDate Instant modificationDate;
|
||||
|
||||
ImmutableAuditableEntityWithVersion(String id, String value, Integer version, Instant modificationDate) {
|
||||
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
this.version = version;
|
||||
this.modificationDate = modificationDate;
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withId(String id) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withValue(String value) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withModificationDate(Instant modificationDate) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
|
||||
ImmutableAuditableEntityWithVersion withVersion(Integer version) {
|
||||
return new ImmutableAuditableEntityWithVersion(id, value, version, modificationDate);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public class MongoTestUtils {
|
||||
|
||||
return Mono.from(database.getCollection(collectionName).drop()) //
|
||||
.delayElement(Duration.ofMillis(10)) // server replication time
|
||||
.then(Mono.from(database.createCollection(collectionName)))
|
||||
.then(Mono.from(database.createCollection(collectionName))) //
|
||||
.delayElement(Duration.ofMillis(10)); // server replication time
|
||||
}
|
||||
|
||||
@@ -124,6 +124,26 @@ public class MongoTestUtils {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all documents from the {@link MongoCollection} with given name in the according {@link MongoDatabase
|
||||
* database}.
|
||||
*
|
||||
* @param dbName must not be {@literal null}.
|
||||
* @param collectionName must not be {@literal null}.
|
||||
* @param client must not be {@literal null}.
|
||||
*/
|
||||
public static void flushCollection(String dbName, String collectionName,
|
||||
com.mongodb.reactivestreams.client.MongoClient client) {
|
||||
|
||||
com.mongodb.reactivestreams.client.MongoDatabase database = client.getDatabase(dbName)
|
||||
.withWriteConcern(WriteConcern.MAJORITY).withReadPreference(ReadPreference.primary());
|
||||
|
||||
Mono.from(database.getCollection(collectionName).deleteMany(new Document())) //
|
||||
.then() //
|
||||
.as(StepVerifier::create) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link com.mongodb.MongoClient} with defaults suitable for replica set usage.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.annotation.Version
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.time.Instant
|
||||
|
||||
@Document("versioned-auditable")
|
||||
data class KAuditableVersionedEntity(
|
||||
@Id val id: String?,
|
||||
val value: String,
|
||||
@Version val version: Long?,
|
||||
@LastModifiedDate val modificationDate: Instant?
|
||||
) {
|
||||
fun withValue(value: String) = copy(value = value)
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<mongo:db-factory dbname="validation" />
|
||||
|
||||
<mongo:mapping-converter base-package="org.springframework.data.mongodb.core" />
|
||||
<mongo:mapping-converter base-package="org.springframework.data.mongodb.core.mapping.event" />
|
||||
|
||||
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
|
||||
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
|
||||
|
||||
Reference in New Issue
Block a user