Question or issue of Kotlin Programming:
As JPA requires, @Entity classes should have a default (non-arg) constructor to instantiate the objects when retrieving them from the database.
In Kotlin, properties are very convenient to declare within the primary constructor, as in the following example:
class Person(val name: String, val age: Int) { /* ... */ }
But when the non-arg constructor is declared as a secondary one it requires values for the primary constructor to be passed, so some valid values are needed for them, like here:
@Entity class Person(val name: String, val age: Int) { private constructor(): this("", 0) }
In case when the properties have some more complex type than just String and Int and they’re non-nullable, it looks totally bad to provide the values for them, especially when there’s much code in primary constructor and init blocks and when the parameters are actively used — when they’re to be reassigned through reflection most of the code is going to be executed again.
Moreover, val-properties cannot be reassigned after the constructor executes, so immutability is also lost.
So the question is: how can Kotlin code be adapted to work with JPA without code duplication, choosing “magic” initial values and loss of immutability?
P.S. Is it true that Hibernate aside of JPA can construct objects with no default constructor?
How to solve this issue?
Solution no. 1:
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:
kotlin-maven-plugin org.jetbrains.kotlin ${kotlin.version} jpa org.jetbrains.kotlin kotlin-maven-noarg ${kotlin.version}
Solution no. 2:
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
Solution no. 3:
@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.
Solution no. 4:
@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)
Solution no. 5:
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)
Solution no. 6:
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() }
Solution no. 7:
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.
Solution no. 8:
As stated above you have to use the no no-arg
plugin provided by Jetbrains.
If you are using Eclispe you may have to edit the Kotlin Compiler Settings.
Window > Preferences > Kotlin > Compiler
Activate the no-arg
Plugin in the Compiler Plugins section.
See: https://discuss.kotlinlang.org/t/kotlin-allopen-plugin-doesnt-work-with-sts/13277/10
Solution no. 9:
Adding the JPA plugin in gradle worked for me:
plugins { id("org.springframework.boot") version "2.3.4.RELEASE" id("io.spring.dependency-management") version "1.0.10.RELEASE" kotlin("jvm") version "1.3.72" kotlin("plugin.spring") version "1.3.72" kotlin("plugin.jpa") version "1.3.72" }
Solution no. 10:
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.
Solution no. 11:
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.
Solution no. 12:
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" } }
Solution no. 13:
Refer to the Interface method mentioned by @tin-ng in the following thread:
hibernate jpa projection with @Query
Convert class Person(val name: String, val age: Int)
to Interface Person{ val name: String val age: Int }