使用JPA的Kotlin:默认构造函数地狱


131

根据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在构造函数执行后无法重新分配-properties,因此也失去了不变性。

所以问题是:如何在不进行代码重复,选择“魔术”初始值和丧失不变性的情况下使Kotlin代码适用于JPA?

PS是真的,除了JPA之外,Hibernate可以使用默认构造函数构造对象吗?


1
INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor)–是的,Hibernate可以在没有默认构造函数的情况下工作。
Michael Piefel '17

它的实现方式是使用设置器-aka:可变性。它实例化默认构造函数,然后查找setter。我想要不可变的对象。唯一可以做的方法是,如果休眠开始查看构造函数。这个hibernate.atlassian.net/browse/HHH-9440
Christian Bongiorno,

Answers:


145

从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>

4
您是否可以扩展一下如何在kotlin代码中使用它,即使是“您data class foo(bar: String)不变”的情况。很高兴看到一个更完整的示例说明如何将其安装到位。谢谢
thecoshman '17

5
这是博客文章,介绍了该博客文章,kotlin-noargkotlin-jpa提供了详细说明其目的的链接blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
Dalibor Filus

1
那么像CustomerEntityPK这样的主键类又如何呢?这不是一个实体,但需要一个默认的构造函数?
jannnik

3
不为我工作。仅当我使构造函数字段为可选时,它才起作用。这意味着该插件无法正常工作。
Ixx

3
@jannnik可以用@Embeddable属性标记主键类,即使您不需要它也可以。这样,它将由拾取kotlin-jpa
svick

33

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

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

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

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


18
您显然没有读过他的问题,否则您会看到他说默认参数不好看的部分,特别是对于更复杂的对象。更不用说,为某些内容添加默认值会隐藏其他问题。
2016年

1
为什么提供默认值是个坏主意?即使使用Java的no args构造函数,也会将默认值分配给字段(例如,对引用类型为null)。
Umesh Rajbhandari '16

1
有时您无法提供合理的默认值。以给定的人为例,您真的应该使用出生日期为它建模,因为它不会改变(当然,例外在某种程度上适用),但是没有明智的默认选择。因此,从纯粹的代码角度来看,您必须将DoB传递给person构造函数,从而确保您永远不会拥有没有有效年龄的人员。问题是,JPA喜欢工作的方式,它喜欢使用无参数构造函数创建对象,然后设置所有内容。
thecoshman

1
我认为这是执行此操作的正确方法,在您不使用JPA也不休眠的其他情况下,此答案也适用。这也是根据答案中提到的文档建议的方法。
Mohammad Rafigh

1
同样,您不应该将数据类与JPA一起使用:“不要将数据类与val属性一起使用,因为JPA并非设计用于不可变类或由数据类自动生成的方法。” spring.io/guides/tutorials/spring-boot-kotlin/...
Tafsen

11

@ 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构造函数并将属性映射到构造函数参数的库插件,而不需要默认构造函数。JacksonKotlin模块可以做到这一点,因此这显然是可能的。

另请参见: https : //stackoverflow.com/a/34624907/3679676,以了解类似的选项。


值得注意的是lateinit和Delegates.notNull()是相同的。
fasth 2016年

4
相似但不一样。如果使用Delegate,它将更改Java对实际字段进行序列化所看到的内容(它将看到委托类)。同样,lateinit当您具有定义良好的生命周期以确保在构造后立即进行初始化时,最好使用它,它适用于这些情况。而委托更适合“首次使用前的某个时间”。尽管从技术上讲它们具有相似的行为和保护,但它们并不相同。
杰森·米纳德'02

如果需要使用原始值,那么我唯一想到的就是在实例化对象时使用“默认值”,这意味着分别使用0和falseInts和Boolean。尽管不确定这将如何影响框架代码
OzzyTheGiant

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

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


1
尽管此代码段可以解决问题,但提供说明确实有助于提高您的帖子质量。请记住,您将来会为读者回答这个问题,而这些人可能不知道您提出代码建议的原因。
DimaSan

@DimaSan,你是对的,但是该帖子已经在某些帖子中进行了解释……
Maksim Kostromin

但是您的代码段是不同的,尽管描述可能有所不同,但是无论如何现在它都更加清晰了。
DimaSan '17

4

没有办法保持这种不变性。构造实例时,必须初始化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()
}

因此,甚至没有办法告诉Hibernate将列映射到构造函数args?好吧,可能是,有一个不需要非arg构造函数的ORM框架/库?:)
热键

不知道这件事,很长时间没有与Hibernate合作。但是应该可以通过命名参数以某种方式实现。
D3xter 2015年

我认为休眠可以通过少量的工作来完成。实际上,在Java 8中,可以在构造函数中命名参数,并且可以像现在将其映射到字段一样进行映射。
Christian Bongiorno

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功能(例如null安全性)。我只是希望将来的JPA和/或Kotlin发行版可以使我们的代码更简单,更好。



2

我自己是一个小人物,但似乎您必须显式初始化并像这样退回到空值

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

1

与@pawelbial相似,我使用了伴随对象来创建默认实例,但是除了定义辅助构造函数外,还可以使用@iolo这样的默认构造函数args。这省去了定义多个构造函数的麻烦,并使代码更简单(尽管允许,定义“ 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类“拥有” TestEntity类的地方不起作用,因为在运行构造函数时不会初始化STUB。



1

如果您添加了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"
    }
}

我正在使用Micronaut,并且可以在1.3.41版中使用。Gradle说我的Kotlin版本是1.3.21,我没有看到任何问题,所有其他插件('kapt / jvm / allopen')都在1.3.21上。我也在使用DSL格式的插件
Gavin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.