Kotlin with JPA: default constructor hell

眉间皱痕 提交于 2019-11-27 17:45:53

As of Kotlin 1.0.6, the kotlin-noarg compiler plugin generates synthetic default construtors for classes that have been annotated with selected annotations.

If you use gradle, applying the kotlin-jpa plugin is enough to generate default constructors for classes annotated with @Entity:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}

apply plugin: "kotlin-jpa"

For Maven:

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>${kotlin.version}</version>

    <configuration>
        <compilerPlugins>
            <plugin>jpa</plugin>
        </compilerPlugins>
    </configuration>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
    </dependencies>
</plugin>

just provide default values for all arguments, Kotlin will make default constructor for you.

@Entity
data class Person(val name: String="", val age: Int=0)

see the NOTE box below the following section:

https://kotlinlang.org/docs/reference/classes.html#secondary-constructors

Jayson Minard

@D3xter has a good answer for one model, the other is a newer feature in Kotlin called lateinit:

class Entity() {
    constructor(name: String, age: Date): this() {
        this.name = name
        this.birthdate = age
    }

    lateinit var name: String
    lateinit var birthdate: Date
}

You would use this when you are sure something will fill in the values at construction time or very soon after (and before first use of the instance).

You will note I changed age to birthdate because you cannot use primitive values with lateinit and they also for the moment must be var (restriction might be released in the future).

So not a perfect answer for immutability, same problem as the other answer in that regard. The solution for that is plugins to libraries that can handle understanding the Kotlin constructor and mapping properties to constructor parameters, instead of requiring a default constructor. The Kotlin module for Jackson does this, so it is clearly possible.

See also: https://stackoverflow.com/a/34624907/3679676 for exploration of similar options.

@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

Initial values are requires if you want reuse constructor for different fields, kotlin doesn't allowed nulls. So whenever you planning omit field, use this form in constructor: var field: Type? = defaultValue

jpa required no argument constructor:

val entity = Person() // Person(name=null, age=null)

there is no code duplication. If you need construct entity and only setup age, use this form:

val entity = Person(age = 33) // Person(name=null, age=33)

there is no magic (just read documentation)

There is no way to keep immutability like this. Vals MUST be initialized when constructing the instance.

One way to do it without immutability is:

class Entity() {
    public constructor(name: String, age: Int): this() {        
        this.name = name
        this.age = age
    }

    public var name: String by Delegates.notNull()

    public var age: Int by Delegates.notNull()
}

I have been working with Kotlin + JPA for quite a while and I have created my own idea how to write Entity classes.

I just slightly extend your initial idea. As you said we can create private argumentless constructor and provide default values for primitives, but when we try need to use another classes it gets a little messy. My idea is to create static STUB object for entity class that you currently writes e.g:

@Entity
data class TestEntity(
    val name: String,
    @Id @GeneratedValue val id: Int? = null
) {
    private constructor() : this("")

    companion object {
        val STUB = TestEntity()
    }
}

and when I have entity class that is related to TestEntity I can easily use stub I just have created. For example:

@Entity
data class RelatedEntity(
        val testEntity: TestEntity,
        @Id @GeneratedValue val id: Long? = null
) {
    private constructor() : this(TestEntity.STUB)

    companion object {
        val STUB = RelatedEntity()
    }
}

Of course this solution is not perfect. You still need to create some boilerplate code that should not be required. Also there is one case that cannot be solved nicely with stubbing - parent-child relation within one entity class - like this:

@Entity
data class TestEntity(
        val testEntity: TestEntity,
        @Id @GeneratedValue val id: Long? = null
) {
    private constructor() : this(STUB)

    companion object {
        val STUB = TestEntity()
    }
}

This code will produce NullPointerException due to chicken-egg issue - we need STUB to create STUB. Unfortunately we need to make this field nullable (or some similar solution) to make code works.

Also in my opinion having Id as last field (and nullable) is quite optimal. We shouldn't assign it by hand and let database do it for us.

I'm not saying that this is perfect solution, but I think that it leverages entity code readability and Kotlin features (e.g. null safety). I just hope future releases of JPA and/or Kotlin will make our code even more simpler and nicer.

Similar to @pawelbial I've used companion object to create a default instance, however instead of defining a secondary constructor, just use default constructor args like @iolo. This saves you having to define multiple constructors and keeps the code simpler (although granted, defining "STUB" companion objects isn't exactly keeping it simple)

@Entity
data class TestEntity(
    val name: String = "",
    @Id @GeneratedValue val id: Int? = null
) {

    companion object {
        val STUB = TestEntity()
    }
}

And then for classes which relate to TestEntity

@Entity
data class RelatedEntity(
    val testEntity: TestEntity = TestEntity:STUB,
    @Id @GeneratedValue val id: Int? = null
)

As @pawelbial has mentioned, this won't work where the TestEntity class "has a" TestEntity class since STUB won't have been initialised when the constructor is run.

I'm a nub myself but seems you have to explicit initializer and fallback to null value like this

@Entity
class Person(val name: String? = null, val age: Int? = null)

These Gradle build lines helped me:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50.
At least, it builds in IntelliJ. It's failing on the command line at the moment.

And I have a

class LtreeType : UserType

and

    @Column(name = "path", nullable = false, columnDefinition = "ltree")
    @Type(type = "com.tgt.unitplanning.data.LtreeType")
    var path: String

var path: LtreeType did not work.

If you added the gradle plugin https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa but did not work, chances are the version is out dated. I was on 1.3.30 and it didn't work for me. After I upgraded to 1.3.41(latest at time of writing), it worked.

Note: kotlin version should be the same as this plugin, eg: this is how I added both:

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!