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)
}
如果属性的类型比 String
和 Int
更复杂,并且它们不可为空,则为它们提供值看起来非常糟糕,尤其是当有主构造函数和 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"
}
根据 JPA 的要求,@Entity
类 应该有一个默认的(非参数)构造函数来在从数据库中检索对象时实例化它们。
在 Kotlin 中,在主构造函数中声明属性非常方便,如下例所示:
class Person(val name: String, val age: Int) { /* ... */ }
但是当非参数构造函数被声明为辅助构造函数时,它需要传递主构造函数的值,因此它们需要一些有效值,如下所示:
@Entity
class Person(val name: String, val age: Int) {
private constructor(): this("", 0)
}
如果属性的类型比 String
和 Int
更复杂,并且它们不可为空,则为它们提供值看起来非常糟糕,尤其是当有主构造函数和 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
@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"
}