spring-projects / spring-data-mongodb

Provides support to increase developer productivity in Java when using MongoDB. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-mongodb/
Apache License 2.0
1.62k stars 1.09k forks source link

Saved object returned by ReactiveMongoTemplate.save(T) is different from the actual saved object in MongoDB for Instant field #4628

Closed Doraemoe closed 8 months ago

Doraemoe commented 8 months ago

Hi,

In the document for ReactiveMongoOperations.save(T) method, it says the returned object of this method is the saved object. However, if the saved object contains Instant field, the value of the saved object this method returns will be different from the actual saved object in MongoDB.

e.g. let's consider a simple db model

@Document(collection = "instant")
class DataForDB(
    @Id var id: ObjectId? = null,
    var name: String? = null,
    val time: Instant? = null,
)

Then a simple test

    companion object {
        val MONGO_CONTAINER = MongoDBContainer(DockerImageName.parse("mongo:latest"))

        @JvmStatic
        @DynamicPropertySource
        fun setProperties(registry: DynamicPropertyRegistry) {
            MONGO_CONTAINER.start()
            registry.add("spring.data.mongodb.uri") {
                "${MONGO_CONTAINER.connectionString}/instant"
            }
        }
    }

    @Autowired
    lateinit var mongoTemplate: ReactiveMongoTemplate

    @Autowired
    lateinit var objectMapper: ObjectMapper

    @Test
    fun shouldEqual(): Unit = runBlocking {
        mongoTemplate.createCollection(DataForDB::class.java).awaitSingle()

        val dataToSave = DataForDB(name = "test", time = Instant.now())
        val saved = mongoTemplate.save(dataToSave).awaitSingleOrNull()

        val query = Query()
            .addCriteria(DataForDB::name.isEqualTo("test"))

        val actual = mongoTemplate.findOne(query, DataForDB::class.java).awaitSingleOrNull()

                // This assert will fail as actual.time will only contains 3 digit precision while saved.time will have 6 digit precision
        assertThat(objectMapper.writeValueAsString(actual?.time)).isEqualTo(objectMapper.writeValueAsString(saved?.time)) 
    }

This is probably because MongoDB provides only millisecond-precision according to this comment If that's the case, shouldn't saved object also returns millisecond-precision?

In addition, when check saved object with other application like mongosh/MongoDB Compass, it shows the saved object time only contains 3 digit precision too.

Am I missing something here? Thanks.

mp911de commented 8 months ago

MongoDB truncates the precision to milliseconds as MongoDB uses millisecond precision in their temporal data types. Newer Java versions have introduced nano-second precision to Instant and other temporal types so this isn't really something we can fix. We also do not plan to load the object after saving it as it would significantly change application and performance behavior.

If you require the WYSIWYG perspective on your object, we suggest you either load the entity after saving it or provide an EntityCallback that truncates instants to milliseconds.

Doraemoe commented 8 months ago

Thanks, I will try use workaround instead. However, shouldn't the document also make it clear since the return is not exactly the saved object?

mp911de commented 8 months ago

There are many ways to affect either the written Document (converters, entity callbacks, lifecycle events) or the entity without reflecting those changes in the other one.