From b2e3e3fb8e97d713ddae9e263161017a7382dde1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Aug 2019 12:13:01 +0200 Subject: [PATCH] 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. --- .../data/mongodb/core/MongoTemplate.java | 4 + .../mongodb/core/ReactiveMongoTemplate.java | 2 +- .../auditing/MongoTemplateAuditingTests.java | 132 +++++++++++++++ .../ReactiveMongoTemplateAuditingTests.java | 158 ++++++++++++++++++ .../mongodb/test/util/MongoTestUtils.java | 22 ++- .../data/mongodb/core/Entities.kt | 32 ++++ ...idatingMongoEventListenerTests-context.xml | 2 +- 7 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/MongoTemplateAuditingTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/ReactiveMongoTemplateAuditingTests.java create mode 100644 spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/Entities.kt diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index d97004f42..e2fe7f05f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1430,6 +1430,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, toSave = maybeEmitEvent(new BeforeConvertEvent(toSave, collectionName)).getSource(); toSave = maybeCallBeforeConvert(toSave, collectionName); + if (source.getBean() != toSave) { + source = operations.forEntity(toSave, mongoConverter.getConversionService()); + } + source.assertUpdateableIdIfNotSet(); MappedDocument mapped = source.toMappedDocument(mongoConverter); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 411107e3e..c04bf27df 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -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)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/MongoTemplateAuditingTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/MongoTemplateAuditingTests.java new file mode 100644 index 000000000..5fcd868d0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/MongoTemplateAuditingTests.java @@ -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); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/ReactiveMongoTemplateAuditingTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/ReactiveMongoTemplateAuditingTests.java new file mode 100644 index 000000000..d0b8d5497 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/auditing/ReactiveMongoTemplateAuditingTests.java @@ -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); + } + + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java index fc4d11d58..cf0b4b011 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java @@ -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. * diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/Entities.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/Entities.kt new file mode 100644 index 000000000..40331c132 --- /dev/null +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/Entities.kt @@ -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) +} diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests-context.xml index 3880b1754..66d9aed36 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests-context.xml @@ -9,7 +9,7 @@ - +