Kotlin 与 JPA:默认构造函数地狱

Kotlin with JPA: default constructor hell

根据 JPA 的要求,@Entity 类 应该有一个默认的(非参数)构造函数来在从数据库中检索对象时实例化它们。

在 Kotlin 中,在主构造函数中声明属性非常方便,如下例所示:

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

但是当非参数构造函数被声明为辅助构造函数时,它需要传递主构造函数的值,因此它们需要一些有效值,如下所示:

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

如果属性的类型比 StringInt 更复杂,并且它们不可为空,则为它们提供值看起来非常糟糕,尤其是当有主构造函数和 init 块中的很多代码以及当参数被积极使用时——当它们通过反射重新分配时,大部分代码将再次执行。

此外,val-属性在构造函数执行后无法重新赋值,因此也失去了不变性。

所以问题是:如何在不重复代码、选择 "magic" 初始值和失去不变性的情况下调整 Kotlin 代码以与 JPA 一起工作?

P.S。除了 JPA 之外,Hibernate 真的可以构造没有默认构造函数的对象吗?

没有办法像这样保持不变性。 构造实例时必须初始化 Vals。

一种没有不变性的方法是:

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()
}

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

当您确定某些东西将在构建时或之后不久(以及首次使用实例之前)填充值时,您可以使用它。

你会注意到我将 age 更改为 birthdate 因为你不能将原始值与 lateinit 一起使用,而且它们目前也必须是 var(限制可能是以后发布)。

所以这不是不变性的完美答案,在这方面与其他答案有同样的问题。解决方案是库插件,可以理解 Kotlin 构造函数并将属性映射到构造函数参数,而不需要默认构造函数。 Kotlin module for Jackson就是这样做的,所以显然是可以的。

另请参阅: 探索类似选项。

只需为所有参数提供默认值,Kotlin 将为您创建默认构造函数。

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

请参阅以下部分下方的 NOTE 框:

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

我使用 Kotlin + JPA 已经有一段时间了,并且我对如何编写 Entity classes.

有了自己的想法

我只是稍微扩展了你最初的想法。正如您所说,我们可以创建私有无参数构造函数并为 原语 提供默认值,但是当我们尝试需要使用另一个 classes 时,它会变得有点混乱。我的想法是为您当前编写的实体 class 创建静态 STUB 对象,例如:

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

    companion object {
        val STUB = TestEntity()
    }
}

并且当我拥有与 TestEntity 相关的实体 class 时,我可以轻松地使用我刚刚创建的存根。例如:

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

    companion object {
        val STUB = RelatedEntity()
    }
}

当然这个方案并不完美。您仍然需要创建一些不需要的样板代码。还有一种情况无法通过存根很好地解决 - 一个实体内的父子关系 class - 像这样:

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

    companion object {
        val STUB = TestEntity()
    }
}

由于先有鸡还是先有蛋的问题,此代码将产生 NullPointerException - 我们需要 STUB 来创建 STUB。不幸的是,我们需要让这个字段可以为空(或一些类似的解决方案)才能使代码正常工作。

我还认为将 Id 作为最后一个字段(并且可为空)是非常理想的。我们不应该手动分配它,让数据库为我们做。

我并不是说这是完美的解决方案,但我认为它利用了实体代码的可读性和 Kotlin 特性(例如 null 安全性)。我只是希望 JPA and/or Kotlin 的未来版本将使我们的代码更加简单和更好。

与@pawelbial 类似,我使用了伴随对象来创建默认实例,但是没有定义辅助构造函数,而是使用默认构造函数参数,如@iolo。这节省了您必须定义多个构造函数并使代码更简单(尽管当然,定义 "STUB" 伴随对象并不能完全保持简单)

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

    companion object {
        val STUB = TestEntity()
    }
}

然后是与 TestEntity

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

正如@pawelbial 所提到的,这在 TestEntity class "has a" TestEntity class 的地方不起作用,因为 STUB 不会当构造函数为 运行 时初始化。

从 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>
@Entity data class Person(/*@Id @GeneratedValue var id: Long? = null,*/
                          var name: String? = null,
                          var age: Int? = null)

如果您想为不同的字段重用构造函数,则需要初始值,kotlin 不允许空值。因此,每当您计划省略字段时,请在构造函数中使用此形式:var field: Type? = defaultValue

jpa 不需要参数构造函数:

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

没有代码重复。如果您需要构造实体并且只需要设置年龄,请使用以下形式:

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

没有魔法(只需阅读文档)

我自己是个小问题,但似乎你必须像这样显式初始化并回退到空值

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

这些 Gradle 构建线帮助了我:
https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa/1.1.50.
至少,它是在 IntelliJ 中构建的。目前它在命令行上失败。

我有一个

class LtreeType : UserType

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

var 路径:LtreeType 无效。

如果您添加了 gradle 插件 https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa 但没有成功,可能是版本已过时。我在 1.3.30 上,它对我不起作用。在我升级到 1.3.41(撰写本文时的最新版本)后,它成功了。

注意:kotlin 版本应与此插件相同,例如:我是这样添加的:

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

如上所述,您必须使用 Jetbrains 提供的无 no-arg 插件。

如果您使用的是 Eclispe,您可能需要编辑 Kotlin 编译器设置。

Window > 首选项 > Kotlin > 编译器

在编译器插件部分激活 no-arg 插件。

参见:https://discuss.kotlinlang.org/t/kotlin-allopen-plugin-doesnt-work-with-sts/13277/10

在 gradle 中添加 JPA 插件对我有用:

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

参考@tin-ng在以下线程中提到的Interface方法:

class Person(val name: String, val age: Int)转换为Interface Person{ val name: String val age: Int }

我正在使用 Intellij 和 运行 kotlin with micronaut 和 gradle kotlin,为了解决这个问题,我需要将这两行添加到我的 build.gradle.kts:

plugins { 
    //This:
    id("org.jetbrains.kotlin.plugin.allopen") version "1.6.10"
    //And this:
    id("org.jetbrains.kotlin.plugin.jpa") version "1.6.10"

    id("com.github.johnrengelman.shadow") version "7.1.1"
    id("io.micronaut.application") version "3.2.2"
}