Search code examples
spring-bootkotlinspring-data-jpaspring-data-restdata-class

Spring Boot Kotlin data class not initailized with default values in REST controller


I have started migrating a Spring Boot application from Java to Kotlin. Before, everything worked perfectly. Now I can't update any Entity received through PUT/POST, because the data doesn't contain the default values from the data class

One entity looks like that:

package org.some.soft.domain

import java.util.*
import javax.persistence.*

@Entity
@Table(name = "book_item")
data class BookItem (

    @ManyToOne(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
    var book: Book,

    @Column(name = "format", nullable = false)
    var format: BookFormat = BookFormat.HARDCOVER,

    @Column(name = "barcode", nullable = false)
    var barcode: String = "0000000000000",

    @Column(name = "label", unique = true)
    var label: String? = null,

    @Column(name = "isReferenceOnly", nullable = false)
    var referenceOnly: Boolean = false,

    @Column(name = "borrowed")
    var borrowed: Date? = null,

    @Column(name = "dueDate")
    var dueDate: Date? = null,

    @Column(name = "price")
    var price: Double? = null,

    @Column(name = "status", nullable = false)
    var status: BookStatus = BookStatus.AVAILABLE,

    @Column(name = "dateOfPurchase")
    var dateOfPurchase: Date = Date(),

    @Column(name = "publicationDate", nullable = false)
    var publicationDate: Date = Date(),

    @Id
    @Column(name = "id", nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    var id: Long? = null,
)

I receive the data through following controller:

    @PutMapping("/book/items")
    fun updateBookItem(@RequestBody book: @Valid BookItem): ResponseEntity<BookItem> {
        val result = bookService.updateBook(book)
        return ResponseEntity
            .ok()
            .headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, book.id.toString()))
            .body(result)
    }

The service just calls the save method from JPA-Repository (bookItemRepository.save(bookItem)) and that's the step where I get the following error:

NULL not allowed for column "PUBLICATION_DATE"; SQL statement:
update book_item set barcode=?, book_id=?, borrowed=?, date_of_purchase=?, due_date=?, format=?, label=?, price=?, publication_date=?, is_reference_only=?, status=? where id=? [23502-214]

Following this tutorial and by copying gradle build from another JHipster project I added the following plugins:

apply plugin: "kotlin" // Required for Kotlin integration
apply plugin: "kotlin-kapt" // Required for annotations processing
apply plugin: "kotlin-spring" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support
apply plugin: "kotlin-allopen" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#using-in-gradle
apply plugin: "kotlin-jpa" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support
apply plugin: "io.gitlab.arturbosch.detekt"

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
    implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlin_version}"


    kapt "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    kapt "org.hibernate:hibernate-jpamodelgen:${hibernateVersion}"
    kapt "org.glassfish.jaxb:jaxb-runtime:${jaxbRuntimeVersion}"

    testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}"

    testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
}

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.MappedSuperclass")
    annotation("javax.persistence.Embeddable")
}

But still I get the issue indicating me, that Spring Boot isn't using my default values from data class. I also debugged, and the attributes are not initialized with their default values.

I also tried changing var to val but the error still persists

Note: I do not send the null values from the client. My request body only contains values that are not null

Thanks very much in advance!


Solution

  • After playing with a few combinations, I finally found the problem.

    JPA uses the no-arg constructor generated by the Gradle plugin by default. When setting default values, the primary constructor has to be a no-arg constructor, which means all parameters have to be optional, so it gets used by JPA.

    Here is what I changed: (I set null as default for book and added the @NotNull annotation)

    @Entity
    @Table(name = "book_item")
    data class BookItem (
    
        @ManyToOne(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
        @NotNull
        var book: Book? = null,
    
        @Column(name = "format", nullable = false)
        var format: BookFormat = BookFormat.HARDCOVER,
    
        @Column(name = "barcode", nullable = false)
        var barcode: String = "0000000000000",
    
        @Column(name = "label", unique = true)
        var label: String? = null,
    
        @Column(name = "isReferenceOnly", nullable = false)
        var referenceOnly: Boolean = false,
    
        @Column(name = "borrowed")
        var borrowed: Date? = null,
    
        @Column(name = "dueDate")
        var dueDate: Date? = null,
    
        @Column(name = "price")
        var price: Double? = null,
    
        @Column(name = "status", nullable = false)
        var status: BookStatus = BookStatus.AVAILABLE,
    
        @Column(name = "dateOfPurchase")
        var dateOfPurchase: Date = Date(),
    
        @Column(name = "publicationDate", nullable = false)
        var publicationDate: Date = Date(),
    
        @Id
        @Column(name = "id", nullable = false)
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
        @SequenceGenerator(name = "sequenceGenerator")
        var id: Long? = null,
    )
    

    This solved all my problems.