与kotlin的休眠–由春靴提供动力

与kotlin的休眠-由春靴提供动力

在这篇文章中,我想在使用kotlin时展示您需要考虑的内容。 Hibernate可能是JVM上的对象关系映射(ORM)最着名的框架,用于在关系数据库中持久地存储普通的旧Java对象(PO​​JOS)。它还实现了 Java Persistence API.,“描述JVM上”描述关系数据管理“的规范。

摘要(TL; DR)

  • Put the Kotlin-Noarg. compiler plugin on your build path, it will generate no-argument constructors for your Hibernate entities.
    • In Gradle, add the following to your buildscript dependencies: classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
    • 可以找到进一步的例子 这里
  • Enable the kotlin-jpa plugin, which works on top of Kotlin-Noarg. 经过 enabling the no-arg generation for Hibernate annotated classes
    • 在Gradle中,激活这样的插件: apply plugin: "kotlin-jpa"
    • 可以找到进一步的例子 这里
  • Put the kotlin-allopen compiler plugin on your build path, and configure it to open classes with entity annotations as Hibernate should not be used with 最后 classes
    • In Gradle, add the following to your buildscript dependencies: classpath "org.jetbrains.kotlin:kotlin-allopen:${versions.kotlin}" 和 add the following configuration:
    allOpen {
        annotation("javax.persistence.Entity")
        annotation("javax.persistence.MappedSuperclass")
        annotation("javax.persistence.Embeddable")
    } 
    
  • Abstract your hashCode/equals implementations in an abstract base class and define entities as ordinary classes inheriting from the abstract base class
    • Do not use data classes to define your @Entity classes - JPA doesn't work well with the generated equals/hashCode functions.

Hibernate实体类型

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

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

让我们看一下有效的实体类需要了解。

Hibernate实体类的要求

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

  • ... 一定是 注释 with the javax.persistence.Entity annotation (or be denoted as such in XML mapping, which we won't consider)
  • ......必须有公共或受保护的(或包裹私人) 没有争论 构造函数。它也可以定义其他构造函数
  • ... 不该是 最后 。没有实体类的方法或持久性的实例变量可以是最终的(从技术上可能但不推荐)
  • ...可以扩展非实体类以及实体类,非实体类可以扩展实体类。两个都 抽象的 具体的 课程可以是实体
  • ......可能提供 javabean. -style属性。这不是必需的,即确定者不是必需的
  • ......必须提供 标识符 attribute (@Id annotation), recommended to use nullable, non-primitive, types
  • ...需要提供有用的实现 equalshashCode (为什么?查找信息 这里 )

If we think about which kind of class in Kotlin best suits these requirements, one might say data classes did. As it turns out though, this is probably not the best solution as discussed in the following.

equals/hashCode dilemma: Don't use data classes as Hibernate entities

It seems to be a good idea to use data classes for defining our Hibernate entities: They basically just need a concise primary constructor with annotated parameters, provide neat things like hashCode, equals, copy, toString out of the box and may be immutable (actually they can't be for Hibernate).

这 re's a problem though: We need to be very careful with auto-generated equals/hashCode functions when working with Hibernate, especially because the entity identifier may be set after the object has been constructed. Actually, using auto-generated IDs means that our classes can never be immutable. Consider the following scenario:

  1. 创建实体的对象 Person
  2. 把这个物体放入一个 HashSet
  3. Persist object via Hibernate (this leads to a generated and 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

这种困境可以通过使用自然钥匙(AKA Business Keys)来修复,即,我们需要找到清楚地识别实体的属性组合。对于一个人来说,这可能是他们的名字和地址,这仍然可能不足。实际上我们没有自然钥匙。此外,由于我们必须将自然关键部件放入数据类以来,它有点令人烦恼地实现数据类。 主要构造函数 和班级身体中的所有其他内容,呼​​叫者必须在构造后设置属性。这感觉不对,所以让我们不这样做......

冬眠建议

什么是冬眠 文件 suggests:

虽然使用A. 自然身份证是最好的 for equalshashCode, sometimes you only have the entity identifier that provides a unique constraint. 可以使用实体标识符进行平等检查, 但它 needs a workaround:
- 你需要提供一个 哈希码的常量值 因此,散列码值在刷新实体之前和之后不会改变。
- 您需要仅对非瞬态实体进行比较实体标识符平等。

这 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 this and still is a valid implementation according to its contract (taken from Oracle Javadocs. ):

hashCode Contract

一般合同 of hashCode is:
- Whenever it is i在同一个物体上nvoked more than once during an execution of a Java application, the hashCode method must 始终如一地返回相同的整数, provided no information used in equals 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 equals(Object) method, then calling the hashCode method 在两个对象中的每一个必须产生相同的整数结果.
- It is not required that if two objects are unequal according to the equals(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 class, 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' equals be based on their identifier and provide a constant value for hashCode. Also, since data classes do not seem to be an adequate solution, we'll be using ordinary, more flexible, classes.

使用Kotlin实现Hibernate实体

As a starter, it feels appropriate to provide a generic base class for our entities that defines an auto-generated identifier and, based on that, implements equals 和 the constant hashCode:


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

这 class AbstractJpaPersistable is pretty straightforward: It defines a generic nullable @Id property, which is going to be auto-generated by Hibernate. The equalshashCode look like discussed earlier. Now we can create our entities based on that class:

@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 classes extend AbstractJpaPersistable<Long> 和 therefore rely on an auto-generated id of type Long.

审查实体要求

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

这 Entity...

  • ... 一定是 注释 with the javax.persistence.Entity annotation (or be denoted as such in XML mapping, which we won't consider) ✔️
  • ......必须有公共或受保护的(或包裹私人) 没有争论 构造函数。它也可以定义其他构造函数❌
  • ......不应该是最终的。没有实体类的方法或持久性的实例变量可以是最终的(从技术上可能但不推荐)❌
  • ...可以扩展非实体类以及实体类,非实体类可以扩展实体类。两个都 抽象的 具体的 课程可以是实体✔️
  • ...可以提供javabean风格的属性。这不是必需的,即确定者不是必需的✔️(我们接受没有定居者)
  • ......必须提供 identifier attribute (@Id annotation), recommended to use nullable, non-primitive, types ✔️
  • ...需要提供有用的实现 equalshashCode ✔️

我们还有两件事要解决:
1. Kotlin classes 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, classes should not be 最后 if possible.
2.到目前为止,我们没有提供一个No-Argument构造函数。

以下将照顾这两个问题。

写一个示例应用程序

设置

现在我们知道如何正确抽象休眠实体,让我们写一个示例应用程序,看看还有更多需要考虑。我们将为我们的应用程序使用Spring Boot Base,这可以轻松生成通过 start.spring.io.:

start.spring.io.
start.spring.io.

(如果您想了解有关春天及其梦幻般的Kotlin支持的更多信息,我鼓励您阅读 这个 blog post as well.)

修复剩余的实体要求

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

此外,我们需要告诉工具,注释它应该应用no-arg构造函数规则。这可以手动完成或通过添加插件来完成 kotlin-jpa to our build, which is "wrapped on top of no-arg. The plugin specifies @Entity, @Embeddable@MappedSuperclass no-arg annotations automatically."

Also, taking care of the 最后 classes problem, we configure the kotlin-allopen plugin to remove the 最后 modifier from all compiled entity classes.

Gradle构建文件

完全,构建脚本看起来像这样(Gradle Groovy DSL):

buildscript {
    ext {
        kotlinVersion = '1.2.60'
        springBootVersion = '2.0.4.RELEASE'
        h2 = '1.4.196'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
        classpath("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: "kotlin-jpa"

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

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.MappedSuperclass")
    annotation("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 Kotlin
    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')

}

我们还可以看到杰克逊,junit5(jupiter)和内存中的一些依赖关系。

这导致具有有效的Hibernate实体的简洁设置:

这 Entity...

  • ... 一定是 注释 with the javax.persistence.Entity annotation (or be denoted as such in XML mapping, which we won't consider) ✔️
  • ......必须有公共或受保护的(或包裹私人) 没有争论 构造函数。它也可以定义其他构造函数✔️
  • ......不应该是最终的。没有实体类的方法或持久性的实例变量可以是最终的(从技术上可能但不推荐)✔️
  • ...可以扩展非实体类以及实体类,非实体类可以扩展实体类。两个都 抽象的 具体的 课程可以是实体✔️
  • ...可以提供javabean风格的属性。这不是必需的,即确定者不是必需的✔️(我们接受不拥有定居者)
  • ......必须提供 identifier attribute (@Id annotation), recommended to use nullable, non-primitive, types ✔️
  • ...需要提供有用的实现 equalshashCode ✔️

一个简单的存储库

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 and we add custom ones by extending the interface with PersonRepository. You can find out more about this mechanism 这里 。您可能猜测,这种抽象存储库定义的实现通过春天发生。
您现在可以继续将此存储库注入控制器并将CRUD API公开给用户。为简单起见,请遵守以下测试用例:

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

    @Test
    fun `basic entity 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. The used SpringExtension adds support for autowired dependencies and, as a result, we can inject the PersonRepository into the test class.
In the test case itself, we create a sample Person object, persist it by using the repository and 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 and that a HashSet works properly with these entities.

进一步的休眠主题

在本文中,我们主要专注于定义Hibernate实体类,因为这项任务可能是最重要的。我们看到需要满足一些约束,编译器插件帮助我们与Kotlin集成了Hibernate。所有展示的东西都只是一套微小的休眠。虽然春天,但是,可以轻松地抽象出Querying和交易处理等许多事情,我们使用了这里。如果你有疑问,我建议阅读 “休眠几乎破坏了我的职业生涯” 非常仔细 - 冬眠可能会显然导致许多头痛;-)。

源代码可以在我的中找到 hibernateonkotlin. GitHub上的存储库。

如果您在申请中使用不同的最佳实践,请随时伸出触手可及。
此外,如果你愿意,请看看我的 推特 帐户和其他 Kotlin相关的帖子 在此页面上,如果您对更多Kotlin的东西感兴趣 -

非常感谢。

13 thoughts on “与kotlin的休眠–由春靴提供动力

发表评论

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