spring-projects / spring-data-relational

Spring Data Relational. Home of Spring Data JDBC and Spring Data R2DBC.
https://spring.io/projects/spring-data-jdbc
Apache License 2.0
766 stars 346 forks source link

MappingInstantiationException when updating a Kotlin data class based entity #1500

Closed hantsy closed 1 year ago

hantsy commented 1 year ago

I tried to update one of my Spring Boot/Spring Data R2dbc example project to the latest Spring Boot 3.0.5 stack. In the original Spring Boot 2.7, the Repository tests worked.

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 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 why it is trying to instantiate an entity when updating the existed entity? In the test, I just want to update the title and status field, not to update the content field, which caused this exception.

When changing the content to nullable, the test is passed. But I think it is unreasonable.

mp911de commented 1 year ago

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.