Search code examples
spring-bootkotlinspring-data-r2dbc

Spring Data R2dbc update failed when upgrading to Spring Boot 3


I tried to update one of my example project to the latest Spring Boot 3.0.5 stack, the original Spring Data R2dbc tests failed.

The R2dbc entity is like this.

@Table(value = "posts")
data class Post(
    @Id
    @Column("id")
    val id: UUID? = null,

    @Column("title")
    var title: String,

    @Column("content")
    var content: String,

    @Column("status")
    var status: Status = Status.DRAFT,

    @Column("created_at")
    @CreatedDate
    val createdAt: LocalDateTime? = null,

    @Column("created_by")
    @CreatedBy
    val createdBy: String? = null,

    @Column("updated_at")
    @LastModifiedDate
    val updatedAt: LocalDateTime? = null,

    @Column("version")
    @Version
    @JsonIgnore
    val version: Long? = null,
)

The PostSummary and PostRepository is:


data class PostSummary(var id: UUID, var title: String)

interface PostRepository :
        CoroutineCrudRepository<Post, UUID>, CoroutineSortingRepository<Post, UUID> {
    fun findByTitleContains(title: String): Flow<PostSummary>
    fun findByStatus(status: Status): Flow<Post>
}

The repository tests is like this.

@OptIn(ExperimentalCoroutinesApi::class)
@DataR2dbcTest
@Testcontainers
@Import(DataConfig::class)
class PostRepositoryTest {
    companion object {
        private val log = LoggerFactory.getLogger(PostRepositoryTest::class.java)

        @Container
        private val postgreSQLContainer: PostgreSQLContainer<*> = PostgreSQLContainer<Nothing>("postgres:12")

        @DynamicPropertySource
        @JvmStatic
        fun registerDynamicProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.r2dbc.url") {
                "r2dbc:postgresql://${postgreSQLContainer.host}:${postgreSQLContainer.firstMappedPort}/${postgreSQLContainer.databaseName}"
            }
            registry.add("spring.r2dbc.username") { postgreSQLContainer.username }
            registry.add("spring.r2dbc.password") { postgreSQLContainer.password }
        }
    }

    @Autowired
    lateinit var dbclient: DatabaseClient

    @Autowired
    lateinit var template: R2dbcEntityTemplate

    @Autowired
    lateinit var posts: PostRepository

    @BeforeEach
    fun setup() = runTest {
        val deleted = template.delete(Post::class.java).all().awaitSingle()
        log.debug("clean posts list before tests: $deleted")
    }

    @Test
    fun testDatabaseClientExisted() {
        assertNotNull(dbclient)
    }

    @Test
    fun testR2dbcEntityTemplateExisted() {
        assertNotNull(template)
    }

    @Test
    fun testPostRepositoryExisted() {
        assertNotNull(posts)
    }

    @Test
    fun testInsertAndQuery() = runTest {
        val data = Post(title = "test title", content = "test content")
        val saved = posts.save(data)
        // verify id is inserted.
        assertNotNull(saved.id)

        val existed = posts.findById(saved.id!!)!!
        log.debug("found existed post: $existed")
        //verify the saved data
        assertThat(existed.title).isEqualTo("test title")
        assertThat(existed.status).isEqualTo(Status.DRAFT)

        existed.apply {
            title = "update title"
            status = Status.PENDING_MODERATION
        }
        posts.save(existed)  // this line caused failure
        val updatedPosts = posts.findByTitleContains("update")

        //verify the updated title
        assertThat(updatedPosts.count()).isEqualTo(1)
        assertThat(updatedPosts.toList()[0].title).isEqualTo("update title")
    }

...
}

I got the following exceptions:

2023-04-15T12:18:33.376+08:00 DEBUG 8320 --- [in @coroutine#2] com.example.demo.PostRepositoryTest      : 
found existed post: Post(id=1ba70b74-8de6-4200-8953-40f180a08c8b, title=test title, content=test content, status=DRAFT, createdAt=2023-04-15T12:18:33.213805, createdBy=null, updatedAt=2023-04-15T12:18:33.213805, version=0)
2023-04-15T12:18:33.528+08:00 DEBUG 8320 --- [in @coroutine#2] o.s.r2dbc.core.DefaultDatabaseClient     : 
Executing SQL statement [UPDATE posts SET title = $1, content = $2, status = $3, created_at = $4, created_by = $5, updated_at = $6, version = $7 WHERE posts.id = $8 AND (posts.version = $9)]
2023-04-15T12:18:33.607+08:00 DEBUG 8320 --- [in @coroutine#2] o.s.r2dbc.core.DefaultDatabaseClient     : 
Executing SQL statement [SELECT posts.id, posts.title FROM posts WHERE posts.title LIKE $1]

org.springframework.data.mapping.model.MappingInstantiationException:
 Failed to instantiate com.example.demo.domain.model.Post 
using constructor fun <init>(java.util.UUID?, kotlin.String, kotlin.String, com.example.demo.domain.model.Status, java.time.LocalDateTime?, kotlin.String?, java.time.LocalDateTime?, kotlin.Long?): 
com.example.demo.domain.model.Post with arguments 
1ba70b74-8de6-4200-8953-40f180a08c8b,update title,null,null,null,null,null,null,248,null

    at org.springframework.data.mapping.model.
KotlinClassGeneratingEntityInstantiator$DefaultingKotlinClassInstantiatorAdapter.createInstance

I am not sure it is trying to instantiate an entity when updating the existed entity? In the test, I just want to update the title/status, not update content, which caused this exception.

Update: change content to nullable, the test is passed. But I think it is unreasonable.

The final select caused the issue, which does not select the content field. Although the query result is a Flow<PostSummary>, but the query still try to instantiate a Post firstly.


Solution

  • Your analysis isn't correct. The update statement passes, and the statement later on SELECT posts.id, posts.title FROM posts doesn't select the content column; Therefore, you cannot instantiate the object.