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

190

根据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属性在构造函数执行后无法重新赋值,因此不可变性也会丢失。

那么问题是:如何适应Kotlin代码以与JPA一起使用,而不会出现代码重复、选择“神奇”的初始值和不可变性丢失的情况?

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


2
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: 类Test没有默认(无参)构造函数:类必须由拦截器实例化。因此,是的,Hibernate可以在没有默认构造函数的情况下工作。 - Michael Piefel
它的实现方式是通过使用setter - 即:可变性。它实例化默认构造函数,然后查找setter。我想要不可变对象。唯一的方法是如果Hibernate开始查看构造函数。这上面有一个开放的票据 https://hibernate.atlassian.net/browse/HHH-9440 - Christian Bongiorno
14个回答

192

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>

5
请问您是否需要一个更完整的示例来说明如何在 Kotlin 代码中使用它,即使只是像 "您的data class foo(bar: String)不会改变" 的情况也可以呈现。感谢您。 - thecoshman
7
这是一篇博客文章,介绍了 kotlin-noargkotlin-jpa 库,并附带了它们的详细用途链接。文章链接为:https://blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here/ - Dalibor Filus
1
那么像CustomerEntityPK这样的主键类呢?它不是一个实体,但需要一个默认构造函数。 - jannnik
7
对我不起作用。只有当我将构造函数字段设为可选时才有效。这意味着插件不起作用。 - User
3
即使你不需要它,你也可以使用@Embeddable属性来标记主键类。这样,它就会被kotlin-jpa识别并使用。 - svick
显示剩余7条评论

38

39
显然你没有读他的问题,否则你就会看到他提到默认参数很难看,尤其是对于更复杂的对象。更不用说,为某些东西添加默认值会隐藏其他问题。 - snowe
1
为什么提供默认值是一个不好的主意?即使使用Java的无参构造函数,字段也会被赋予默认值(例如引用类型为null)。 - Umesh Rajbhandari
3
有时候你无法提供一个合理的默认值。以人这个例子来说,你应该用出生日期作为模型,因为它不会改变(当然,总有例外)。但是并不存在一个合理的默认值可供使用。因此从纯代码角度来看,你必须在构造函数中传入一个出生日期,确保你永远不会创建一个年龄无效的人。问题是,JPA 喜欢这样操作:首先创建一个无参构造函数的对象,然后设置所有属性。 - thecoshman
1
我认为这是正确的方法,这个答案也适用于你不使用JPA或Hibernate的情况。此外,根据答案中提到的文档,这也是建议的方法。 - Mohammad Rafigh
2
此外,您不应该在JPA中使用数据类:“不要使用具有val属性的数据类,因为JPA未设计用于与不可变类或由数据类自动生成的方法一起使用。” https://spring.io/guides/tutorials/spring-boot-kotlin/#_persistence_with_jpa - Stianhn
目前,我使用的是普通的 class 而不是 data class,这是 JPA 的方式。 - iolo

24

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

12

@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构造函数并将属性映射到构造函数参数,而不需要默认构造函数。 Jackson的Kotlin模块就是这样做的,因此显然是可能的。

另请参阅:探索类似选项的https://dev59.com/bF0Z5IYBdhLWcg3wzC9W#34624907


值得注意的是,lateinit 和 Delegates.notNull() 是相同的。 - fasth
4
类似但并非完全相同。如果使用委托,它会改变 Java 对实际字段序列化时所看到的内容(它会看到委托类)。此外,当您有一个明确定义的生命周期保证在构造后很快进行初始化时,最好使用 lateinit,它是为这些情况而设计的。而委托更适用于“在首次使用之前的某个时候”。尽管从技术上讲,它们具有类似的行为和保护措施,但它们并不相同。 - Jayson Minard
如果您需要使用原始值,我能想到的唯一方法就是在实例化对象时使用“默认值”,这意味着对于整数和布尔值分别使用0和false。不确定这会如何影响框架代码。 - OzzyTheGiant

5
@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)

没有什么神奇的(只需阅读文档)


2
虽然这段代码可能解决了问题,但包括解释真的有助于提高您文章的质量。请记住,您正在为未来的读者回答问题,而这些人可能不知道您的代码建议的原因。 - DimaSan
@DimaSan,你是对的,但那个线程已经在一些帖子中有了解释... - Maksim Kostromin
1
但是你的代码片段不同,尽管可能有不同的描述,但现在它更清晰了。 - DimaSan

4

这样是无法保持不可变性的。当构建实例时,必须初始化值。

一种不使用不可变性的方法是:

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

那么,甚至没有办法告诉Hibernate将列映射到构造函数参数中?嗯,也许有一个ORM框架/库不需要无参构造函数? :) - hotkey
不太确定,很久没用过Hibernate了。但是应该可以通过命名参数来实现。 - D3xter
我认为Hibernate可以通过一些(不多)的工作来实现这个。在Java 8中,您实际上可以在构造函数中命名参数,并且这些参数可以像现在映射到字段一样映射。 - Christian Bongiorno

4

我本身是一个新手,但似乎你需要显式初始化器并回退到null值,就像这样:

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

3

3

我已经使用Kotlin + JPA工作了相当长的时间,并且我已经想出了自己关于如何编写实体类的想法。

我只是稍微扩展了您最初的想法。正如您所说,我们可以创建私有无参构造函数并为原始类型提供默认值,但是当我们尝试使用另一个类时,会变得有些混乱。我的想法是为当前编写的实体类创建静态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。不幸的是,我们需要将这个字段设置为可空(或者采用类似的解决方案)才能使代码正常工作。
此外,我认为将Id作为最后一个字段(并且可为空)非常理想。我们不应该手动分配它,而应该让数据库为我们完成。
我并不是说这是完美的解决方案,但我认为它提高了实体代码的可读性以及Kotlin的特性(例如空安全)。我只希望未来的JPA和/或Kotlin版本能够使我们的代码更加简单和优雅。

2
使用数据类作为实体对您的健康非常不利:https://www.jpa-buddy.com/blog/best-practices-and-common-pitfalls/ - kraxor

2

我正在使用Intellij运行kotlin,使用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"
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接