Kotlin 缺省构造函数

根据 JPA 的要求,@Entity类应该有一个默认(非 arg)构造函数,以便在从数据库检索对象时实例化这些对象。

在 Kotlin,属性很容易在主构造函数中声明,如下例所示:

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

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

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

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

而且,在构造函数执行之后,val-属性不能被重新分配,因此不可变性也会丢失。

因此,问题是: Kotlin 代码如何适应 JPA 而不需要代码复制、选择“魔术”初始值和不可变性的损失?

另外,除了 JPA,Hibernate 真的可以构造没有缺省构造函数的对象吗?

70768 次浏览

没有办法像这样保持不变性。 构造实例时必须初始化 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 构造函数和将属性映射到构造函数参数的问题,而不需要缺省构造函数。给杰克逊的科特林模块做到了这一点,所以这显然是可能的。

参见: https://stackoverflow.com/a/34624907/3679676以探索类似的选项。

只要为所有参数提供默认值,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 类。

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

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


companion object {
val STUB = TestEntity()
}
}

当我有实体类是相关的 TestEntity我可以很容易地使用存根我刚刚创建。例如:

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


companion object {
val STUB = RelatedEntity()
}
}

当然,这个解决方案并不完美。您仍然需要创建一些不需要的样板代码。还有一种情况不能很好地用一个实体类中的存根-父-子关系来解决——像这样:

@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。不幸的是,我们需要使这个字段为空(或者一些类似的解决方案) ,以使代码工作。

同样在我看来,将 身份证作为最后一个字段(并且可以为空)是相当理想的。我们不应该手工分配它,让数据库为我们做它。

我并不是说这是一个完美的解决方案,但是我认为它利用了实体代码的可读性和 Kotlin 特性(例如空安全)。我只是希望 jPA 和/或 Kotlin 的未来版本能让我们的代码更简单、更好。

与@pawelbian 类似,我使用伴侣对象来创建一个默认实例,但是不要定义辅助构造函数,只要使用像@iolo 这样的缺省构造函数参数即可。这样可以省去您定义多个构造函数的麻烦,并使代码更加简单(尽管可以肯定,定义“ STUB”伴随对象并不能使代码更加简单)

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


companion object {
val STUB = TestEntity()
}
}

然后是与 TestEntity相关的类

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

正如@pawelbian 所提到的,在 TestEntity类“有一个”TestEntity类的情况下,这种方法不起作用,因为当构造函数运行时,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"

对于玛文:

<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 不允许使用 null。因此,当您计划省略字段时,请在构造函数 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)

如果你添加了渐变插件 https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.jpa,但没有工作,机会是版本过时。我当时是1分30秒,但是没有用。在我升级到1.3.41(最晚在编写本文时)之后,它工作了。

注意: kotlin 版本应该与这个插件相同,例如: 这是我如何添加两者:

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

如上所述,你必须使用 Jetbrain 提供的 no no-arg插件。

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

窗口 > 首选项 > 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 方法:

使用@Query 进行 hibernate jpa 投影

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

我使用 Intellij 运行 microaut 和 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"
}