跳到内容

与Kotlin休眠–由Spring Boot提供支持

152018年8月 通过 s1m0nw1 13条留言

与Kotlin休眠-由Spring Boot提供支持

在本文中,我想说明在将Kotlin与Hibernate结合使用时需要考虑的事项。 Hibernate可能是JVM上最著名的对象关系映射(ORM)框架,用于在关系数据库中持久存储Plain Old Java Objects(POJO)。它还实现了 Java持久性API是JVM上的“描述关系数据的管理”规范。

摘要(TL; DR)

  • Put the 科特林-Noarg compiler plugin on your build path, it will generate 无争辩 constructors for your Hibernate entities.
    • In Gradle, add the following to your buildscript dependencies: 类path("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
    • 可以找到更多示例 这里
  • Enable the 科特林-jpa plugin, which works on top of 科特林-Noarg 通过 enabling the no-arg generation for Hibernate 带注释 类es
    • 在Gradle中,如下激活插件: apply plugin: "科特林-jpa"
    • 可以找到更多示例 这里
  • Put the 科特林-allopen compiler plugin on your build path, 和 configure it to open 类es with 实体 注解s as Hibernate should not be used with 最后 类es
    • In Gradle, add the following to your buildscript dependencies: 类path "org.jetbrains.kotlin:kotlin-allopen:${versions.kotlin}" 和 add the following configuration:
    allOpen {
        注解("javax.persistence.Entity")
        注解("javax.persistence.MappedSuperclass")
        注解("javax.persistence.Embeddable")
    } 
    
    • 可以找到更多示例 这里
  • Abstract your hashCode/等于 implementations in an 抽象 base 类 和 define entities as ordinary 类es inheriting from the 抽象 base 类
    • Do not use 资料类别es to define your @Entity 类es - JPA doesn't work well with the generated 等于/hashCode functions.

休眠实体类型

将Hibernate集成到应用程序中时,我们需要做的最重要的事情是定义我们要保留的实体类型,即定义表和类之间的映射。冬眠 文件资料 describes an "Entity" as follows:

实体 类型描述 映射 between the actual persistable domain model object 和 a database table row. To avoid any confusion with the 注解 that marks a given 实体 type, the 注解 will be further referred as @Entity.

让我们看一下有效实体类的外观。

实体类的休眠要求

Hibernate imposes certain requirements on a valid Entity type: An 实体...

  • ... 一定是 带注释 with the javax.persistence.Entity 注解 (or be denoted as such in XML 映射, which we won't consider)
  • ...必须具有公共或受保护的(或打包私有的) 无争辩 构造函数。它也可以定义其他构造函数
  • ... 不该是 最后。实体类的任何方法或持久实例变量都不得是最终的(技术上可行,但不建议这样做)
  • ...可以扩展非实体类以及实体类,并且非实体类可以扩展实体类。都 抽象具体 类可以是实体
  • ...可能会提供 JavaBean样式的属性。这不是必须的,即不需要二传手
  • ...必须提供 识别码 attribute (@Id 注解), recommended to use nullable, non-primitive, types
  • ...需要提供有用的实现 等于hashCode (为什么?查找信息 这里)

If we think about which kind of 类 in 科特林 best suits these requirements, one might say data 类es did. As it turns out though, 这个 is probably not the best solution as discussed in the following.

等于/hashCode dilemma: Don't use data 类es as Hibernate entities

It seems to be a good idea to use 资料类别es for defining our Hibernate entities: 的y basically just need a concise 主要建设者 with 带注释 parameters, provide neat things like hashCode, 等于, copy, toString out of the box 和 may be immutable (actually they can't be for Hibernate).

的re's a problem though: We need to be very careful with auto-generated 等于/hashCode functions when working with Hibernate, especially because the 实体 识别码 may be set after the object has been constructed. Actually, using auto-generated IDs means that our 类es can never be immutable. Consider the following scenario:

  1. 创建实体的对象 Person
  2. 将此对象放入 HashSet
  3. Persist object via Hibernate (this leads to a generated 和 updated Person::id 和 thus changes its hashCode)
  4. Test if the object still exists in the HashSet will yield false since the hash code changed

可以通过使用自然键(也称为业务键)来解决此难题,即我们需要找到明确标识实体的属性组合。对于一个人来说,这可能是他们的姓名和地址,可能仍然不够。实际上,我们没有每个实体的自然键。另外,使用数据类来实现这种行为有点麻烦,因为我们必须将自然关键部分放入 主要建设者 以及类主体中的所有其他内容,调用者必须在构造后设置属性。这感觉不对,所以我们就不要做...

休眠建议

什么冬眠 文件资料 建议:

虽然使用 natural-id最好 for 等于hashCode, sometimes you only have the 实体 识别码 that provides a unique constraint. 可以使用实体标识符进行相等性检查, 但它 needs a workaround:
-您需要提供一个 hashCode的常量值 因此,在刷新实体前后,哈希码值不会更改。
-您只需要比较非瞬态实体的实体标识符相等性。

的y say that we can use the Hibernate-generated ID for equality checks as long as we provide a "constant value" for hashCode. This is because, reviewing the example scenario from earlier, the hash code should not change for an object once it's been put into hash-based collections. Using a constant value for hashCode fixes 这个 和 still is a valid implementation according to its contract (taken from Oracle Java文档):

hashCode 合同

总合同 of hashCode is:
-每当我在同一个对象上 more than once during an execution of a Java application, the hashCode method must 始终返回相同的整数, provided no information used in 等于 comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
- If two objects are equal according to the 等于(Object) method, then calling the hashCode method 在两个对象中的每个对象上必须产生相同的整数结果.
- It is not required that if two objects are unequal according to the 等于(Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects 可以提高哈希表的性能。

因此,这一切都很好,尽管我们需要仔细查看该合同的最后一句话:

但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

hashCode 性能影响

If we decide to yield constant values from hashCode for any object of a 类, performance will suffer. You cannot expect hash collections to work as efficient as with properly distributed hash codes:

这个实现 [HashMap] provides constant-time performance for the basic operations (getput), 假设哈希函数将元素正确分散在存储桶中.

如果您可以解决这些性能问题,则可以遵循上述方法。对于我们来说,这不算问题。

As a result, we want to let our entities' 等于 be based on their 识别码 和 provide a constant value for hashCode. Also, since data 类es do not seem to be an adequate solution, we'll be using ordinary, more flexible, 类es.

使用Kotlin实施Hibernate实体

As a starter, it feels appropriate to provide a generic base 类 for our entities that defines an auto-generated 识别码 和, based on that, implements 等于 和 the constant hashCode:


@MappedSuperclass abstract 类 AbstractJpaPersistable<T : Serializable> { companion object { private val serialVersionUID = -5554308939380869754L } @Id @GeneratedValue private var id: T? = null override fun getId(): T? { return id } override fun 等于(other: Any?): Boolean { other ?: return false if (this === other) return true if (javaClass != ProxyUtils.getUserClass(other)) return false other as AbstractJpaPersistable<*> return if (null == 这个.getId()) false else 这个.getId() == other.getId() } override fun hashCode(): Int { return 31 } override fun toString() = "Entity of type ${this.javaClass.name} with id: $id" }

的 类 AbstractJpaPersistable is pretty straightforward: It defines a generic nullable @Id property, which is going to be auto-generated 通过 Hibernate. 的 等于hashCode look like discussed earlier. Now we can create our entities based on that 类:

@Entity
class Person(
    val name: String,
    @OneToOne(cascade = [(CascadeType.ALL)], orphanRemoval = true, fetch = FetchType.EAGER)
    val address: Address
) : AbstractJpaPersistable<Long>()

@Entity
class Address(
    val street: String,
    val zipCode: String,
    val city: String
) : AbstractJpaPersistable<Long>()

We can see two rather simple entities: A Person which has an associated Address. Both @Entity 类es extend AbstractJpaPersistable<Long> 和 therefore rely on an auto-generated id of type Long.

审查实体要求

如前所述,我们对需要考虑的实体有一些要求。让我们回顾一下上面的方法已经解决的问题:

实体...

  • ... 一定是 带注释 with the javax.persistence.Entity 注解 (or be denoted as such in XML 映射, which we won't consider) ✔️
  • ...必须具有公共或受保护的(或打包私有的) 无争辩 构造函数。它也可以定义其他构造函数❌
  • ...不应该是最终的。实体类的任何方法或持久实例变量都不得是最终的(技术上可行,但不建议这样做)❌
  • ...可以扩展非实体类以及实体类,并且非实体类可以扩展实体类。都 抽象具体 类可以是实体✔️
  • ...可能提供JavaBean样式的属性。这不是必须的,即不需要二传手✔️(我们接受没有二传手)
  • ...必须提供 识别码 attribute (@Id 注解), recommended to use nullable, non-primitive, types ✔️
  • ...需要提供有用的实现 等于hashCode ✔️

我们还有两件事要解决:
1. 科特林 类es are 最后 通过 default, which is good practice in most cases but Hibernate does not really like that. Since it makes use of proxies that allow e.g. lazy-loading of entities, 类es should not be 最后 if possible.
2.到目前为止,我们没有提供无参数的构造函数。

以下内容将解决这两个问题。

编写示例应用程序

设定

现在我们知道了如何正确抽象Hibernate实体,让我们编写一个示例应用程序,看看是否还有更多需要考虑的事情。我们将为应用程序使用Spring Boot基础,可以通过以下方式轻松生成 start.spring.io:

start.spring.io
start.spring.io

(如果您想了解有关Spring及其对Kotlin的出色支持的更多信息,我鼓励您阅读 这个 博客文章。)

修正剩余实体需求

如前所述,Hibernate期望为其实体定义一个无参数的构造函数。由于我们不想在编译时提供一个,因此我们使用JetBrains的编译器插件,称为 科特林-Noarg,它“为具有特定批注的类生成一个附加的零参数构造函数。生成的构造函数是合成的,因此不能直接从Java或Kotlin调用,但是可以使用反射调用。”

另外,我们需要告诉工具应在其上应用no-arg构造函数规则的注释。这可以手动完成,也可以添加插件 科特林-jpa to our build, which is "wrapped on top of no-arg. 的 plugin specifies @Entity, @Embeddable@MappedSuperclass no-arg 注解s automatically."

Also, taking care of the 最后 类es problem, we configure the 科特林-allopen plugin to remove the 最后 modifier from all compiled 实体 类es.

Gradle构建文件

总体而言,构建脚本如下所示(Gradle Groovy DSL):

buildscript {
    ext {
        科特林Version = '1.2.60'
        弹簧BootVersion = '2.0.4.RELEASE'
        h2 = '1.4.196'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        类path("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        类path("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        类path("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
        类path("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: "科特林-jpa"

group = 'com.kotlinexpertise'
version = '0.0.1-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

allOpen {
    注解("javax.persistence.Entity")
    注解("javax.persistence.MappedSuperclass")
    注解("javax.persistence.Embeddable")
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    // Database Drivers
    compile("com.h2database:h2:$h2")

    //Jackson 科特林
    compile('com.fasterxml.jackson.module:jackson-module-kotlin')

    //Junit 5
    testCompile('org.springframework.boot:spring-boot-starter-test') {
        exclude module: 'junit'
    }
    testImplementation('org.junit.jupiter:junit-jupiter-api')
    testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')

    testCompile('org.springframework.boot:spring-boot-starter-test')

}

我们还可以看到Jackson,Junit5(木星)和内存H2数据库的一些其他依赖关系。

这将导致使用有效的Hibernate实体进行简洁的设置:

实体...

  • ... 一定是 带注释 with the javax.persistence.Entity 注解 (or be denoted as such in XML 映射, which we won't consider) ✔️
  • ...必须具有公共或受保护的(或打包私有的) 无争辩 构造函数。它也可以定义其他构造函数✔️
  • ...不应该是最终的。实体类的任何方法或持久实例变量都不得是最终的(技术上可行,但不建议这样做)✔️
  • ...可以扩展非实体类以及实体类,并且非实体类可以扩展实体类。都 抽象具体 类可以是实体✔️
  • ...可能提供JavaBean样式的属性。这不是必须的,即不需要二传手✔️(我们接受没有二传手)
  • ...必须提供 识别码 attribute (@Id 注解), recommended to use nullable, non-primitive, types ✔️
  • ...需要提供有用的实现 等于hashCode ✔️

一个简单的存储库

Thanks to Spring, the implementation for a repository that exposes Person entities is quite easy:

interface PersonRepository : JpaRepository<Person, Long> {
    fun getByAddressStreet(street: String): Person?
}

的 interface org.springframework.data.jpa.repository.JpaRepository defines common CRUD operations 和 we add custom ones 通过 extending the interface with PersonRepository. You can find out more about 这个 mechanism 这里。您可能会猜到,这种抽象存储库定义的实现是通过Spring实现的。
现在,您可以继续将此仓库注入控制器中,并向用户公开CRUD API。为简单起见,请遵循以下测试案例:

@ExtendWith(SpringExtension::class)
@SpringBootTest
class HibernateDemoApplicationTests(@Autowired val repo: PersonRepository) {

    @Test
    fun `basic 实体 checks`() {
        val p = Person("Paul", Address("HelloStreet", "A-55", "Paris"))
        val hashCodeBefore = p.hashCode()
        val personSet = hashSetOf(p)
        repo.save(p)
        val hashCodeAfter = p.hashCode()
        assertThat(repo.findAll()).hasSize(1)
        assertThat(personSet).contains(p)
        assertThat(hashCodeAfter).isEqualTo(hashCodeBefore)
    }
}

This test runs on JUnit 5, which allows constructor injection for certain objects. 的 used SpringExtension adds support for autowired dependencies 和, as a result, we can inject the PersonRepository into the test 类.
In the test case itself, we create a sample Person object, persist it 通过 using the repository 和 then verify that it can be found via findAll. Assertions are based on org.assertj.
In addition, the test verifies that the hashCode for a Person does not change after it got persisted through Hibernate 和 that a HashSet works properly with these entities.

进一步的休眠主题

在本文中,我们主要集中于定义Hibernate实体类,因为此任务可能是最重要的任务。我们看到需要满足一些约束条件,并且编译器插件可以帮助我们将Hibernate与Kotlin集成。所有演示的内容只是一小部分的Hibernate。不过,由于有了Spring,许多查询和事务处理之类的事情都可以轻松抽象出来,我们在这里使用了它们。如果您有疑问,建议阅读 “冬眠几乎毁了我的事业” 非常小心-休眠会明显导致许多头痛;-)。

源代码可以在我的 冬眠的OnKotlin GitHub上的存储库。

如果您在应用程序中使用了不同的最佳实践,请不要犹豫。
另外,如果你愿意,看看我的 推特 帐户及其他 科特林相关职位 在此页面上,如果您对更多Kotlin物品ðŸ™,有兴趣,请关注

非常感谢。

关于13条想法“与Kotlin休眠–由Spring Boot提供支持

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *