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 @@ - +