【问题标题】:Kotlin with JPA: default constructor hell带有 JPA 的 Kotlin:默认构造函数地狱
【发布时间】:2015-11-09 08:51:51
【问题描述】:

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

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

所以问题是:如何在不重复代码、选择“神奇”初始值和丧失不变性的情况下使 Kotlin 代码适应 JPA?

附:除了 JPA 之外,Hibernate 真的可以构造没有默认构造函数的对象吗?

【问题讨论】:

  • INFO -- org.hibernate.tuple.PojoInstantiator: HHH000182: No default (no-argument) constructor for class: Test (class must be instantiated by Interceptor) - 所以,是的,Hibernate 可以在没有默认构造函数的情况下工作。
  • 使用 setter 的方式 - 又名:可变性。它实例化默认构造函数,然后查找设置器。我想要不可变的对象。唯一可以做到的方法是hibernates开始查看构造函数。这个hibernate.atlassian.net/browse/HHH-9440有一张公开的票

标签: hibernate jpa default-constructor kotlin


【解决方案1】:

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

hibernate jpa projection with @Query

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

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

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

      窗口 > 首选项 > Kotlin > 编译器

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

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

      【讨论】:

        【解决方案4】:

        如果您添加了 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 格式
        【解决方案5】:

        这些 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 不起作用。

        【讨论】:

          【解决方案6】:

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

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

          【讨论】:

            【解决方案7】:
            @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)
            

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

            【讨论】:

            • 虽然这段代码 sn-p 可以解决问题,including an explanation 确实有助于提高您的帖子质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
            • @DimaSan,你是对的,但该线程已经在一些帖子中进行了解释......
            • 但是你的sn-p是不同的,虽然可能有不同的描述,反正现在清楚多了。
            【解决方案8】:

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

            【讨论】:

            • 您能否扩展一下如何在您的 kotlin 代码中使用它,即使这是“您的 data class foo(bar: String) 没有改变”的情况。很高兴看到一个更完整的例子来说明它是如何适应的。谢谢
            • 这是介绍 kotlin-noargkotlin-jpa 的博客文章,其中包含详细说明其用途的链接 blog.jetbrains.com/kotlin/2016/12/kotlin-1-0-6-is-here
            • 那么像CustomerEntityPK这样的主键类,它不是实体但需要一个默认构造函数呢?
            • 不适合我。它仅在我将构造函数字段设为可选时才有效。这意味着该插件无法正常工作。
            • @jannnik 您可以使用@Embeddable 属性标记主键类,即使您不需要它。这样,它就会被kotlin-jpa接收。
            【解决方案9】:

            类似于@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 类“具有”TestEntity 类的情况下不起作用,因为在运行构造函数时,STUB 不会被初始化。

            【讨论】:

              【解决方案10】:

              我使用 Kotlin + JPA 已经有一段时间了,我已经创建了自己的想法来编写实体类。

              我只是稍微扩展了您最初的想法。正如您所说,我们可以创建私有无参数构造函数并为 primitives 提供默认值,但是当我们尝试需要使用其他类时,它会变得有点混乱。我的想法是为您当前编写的实体类创建静态 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 的未来版本能够让我们的代码更加简单和美观。

              【讨论】:

                【解决方案11】:

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

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

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

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

                【讨论】:

                • 您显然没有阅读他的问题,否则您会看到他指出默认参数看起来很糟糕的部分,尤其是对于更复杂的对象。更不用说,为某些东西添加默认值会隐藏其他问题。
                • 为什么提供默认值是个坏主意?即使使用 Java 的 no args 构造函数,也会将默认值分配给字段(例如,将 null 分配给引用类型)。
                • 有时您无法提供合理的默认值。以一个人的给定示例为例,您确实应该使用出生日期对其进行建模,因为它不会改变(当然,例外情况在某些地方适用)但是没有合理的默认设置。因此,从纯代码的角度来看,您必须将 DoB 传递给 person 构造函数,从而确保您永远不会拥有一个没有有效年龄的人。问题是,JPA 喜欢工作的方式,它喜欢用无参数构造函数创建一个对象,然后设置所有内容。
                • 我认为这是正确的方法,这个答案适用于您不使用 JPA 或休眠的其他情况。根据答案中提到的文件,这也是建议的方式。
                • 另外,您不应该将数据类与 JPA 一起使用:“不要将数据类与 val 属性一起使用,因为 JPA 并非旨在与不可变类或数据类自动生成的方法一起使用。” spring.io/guides/tutorials/spring-boot-kotlin/…
                【解决方案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 构造函数并将属性映射到构造函数参数,而不需要默认构造函数。 Kotlin module for Jackson 做到了这一点,所以这显然是可能的。

                另请参阅:https://stackoverflow.com/a/34624907/3679676 了解类似选项的探索。

                【讨论】:

                • 值得注意的是,lateinit 和 Delegates.notNull() 是一样的。
                • 类似但不一样。如果使用 Delegate,它会更改 Java 对实际字段的序列化所看到的内容(它看到的是委托类)。此外,当您有一个明确定义的生命周期以保证在构造后不久进行初始化时,最好使用lateinit,它适用于这些情况。而委托更适用于“首次使用之前的某个时间”。尽管在技术上它们具有相似的行为和保护,但它们并不完全相同。
                • 如果您需要使用原始值,我唯一能想到的就是在实例化对象时使用“默认值”,我的意思是使用 0 和 false 表示 Ints 和布尔值。不知道这会如何影响框架代码
                【解决方案13】:

                没有办法像这样保持不变性。 构造实例时必须初始化 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?好吧,可能有一个不需要非参数构造函数的 ORM 框架/库? :)
                • 不确定,很久没用Hibernate了。但是应该可以以某种方式使用命名参数来实现。
                • 我认为 hibernate 可以通过一点(不多)的工作来做到这一点。在 java 8 中,您实际上可以在构造函数中命名参数,这些参数可以像现在一样映射到字段。
                猜你喜欢
                • 2018-04-23
                • 2019-08-09
                • 2017-04-08
                • 1970-01-01
                • 2022-01-26
                • 1970-01-01
                • 2016-03-18
                • 2023-03-20
                相关资源
                最近更新 更多