developer tip

JPA가있는 Kotlin : 기본 생성자 지옥

optionbox 2020. 8. 4. 07:32
반응형

JPA가있는 Kotlin : 기본 생성자 지옥


JPA에서 요구하는대로 @Entity클래스는 데이터베이스에서 객체를 검색 할 때 객체를 인스턴스화 할 기본 (비 인수) 생성자를 가져야합니다.

Kotlin에서는 다음 예제와 같이 기본 생성자 내에서 속성을 선언하는 것이 매우 편리합니다.

class Person(val name: String, val age: Int) { /* ... */ }

그러나 인수가 아닌 생성자를 2 차 생성자로 선언하면 기본 생성자가 전달되어야하므로 다음과 같이 유효한 값이 필요합니다.

@Entity
class Person(val name: String, val age: Int) {
    private constructor(): this("", 0)
}

경우 속성은보다 좀 더 복잡한 유형이있을 때 StringInt특히 주 생성자에서 많은 코드있을 때, 그들에 대한 값을 제공하는 완전히 나쁜 보이는, 그들은 Null이있어 init블록 매개 변수가 적극적으로 사용된다 - -리플렉션을 통해 재 할당 될 때 대부분의 코드가 다시 실행됩니다.

또한 val생성자가 실행 된 후에는 -properties를 다시 지정할 수 없으므로 불변성도 손실됩니다.

문제는 어떻게 코드 중복없이 Kotlin 코드를 JPA와 함께 사용하여 "마법의"초기 값과 불변성의 손실을 선택할 수 있습니까?

추신 JPA를 제외하고 Hibernate가 기본 생성자없이 객체를 생성 할 수 있다는 것이 사실입니까?


Kotlin 1.0.6 부터 kotlin-noarg컴파일러 플러그인은 선택된 주석으로 주석이 달린 클래스에 대한 합성 기본 구성자를 생성합니다.

gradle을 사용하는 경우 kotlin-jpa플러그인을 적용하면 @Entity다음으로 주석이 달린 클래스의 기본 생성자를 생성 할 수 있습니다 .

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

apply plugin: "kotlin-jpa"

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>

모든 인수에 기본값을 제공하면 Kotlin이 기본 생성자를 만듭니다.

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

NOTE다음 섹션 아래 상자를 참조하십시오 .

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


@ D3xter는 한 모델에 대한 좋은 대답을 가지고 있으며 다른 모델은 Kotlin의 새로운 기능입니다 lateinit.

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

    lateinit var name: String
    lateinit var birthdate: Date
}

구성 시간 또는 그 직후 (인스턴스를 처음 사용하기 전에) 값이 채워질 것으로 확신 할 때이 옵션을 사용합니다.

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.


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)

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.


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"
    }
}

참고URL : https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell

반응형