使用带有Kotlin的JPA(Hibernate)的优秀实践

译文
开发 前端
本文将和您讨论,作为JPA的经典实现—Hibernate,是如何避免各种常见的错误,以及各种与Kotlin有关的优秀实践。

[[413599]]

【51CTO.com快译】作为适合多平台应用的静态编程语言,Kotlin比Java更简洁、更有表现力、也更具有代码的安全性。同时,Kotlin提供了与Java的无缝互操作性。也就是说,Java允许开发人员将他们的项目迁移到Kotlin处,而无需重写整个代码库。当然,针对此类迁移,我们可能需要在Kotlin应用中使用JPA(Java Persistence API)。虽然许多开发者普遍认为:没有JPA,就不存在实体。但是他们在Kotlin中定义JPA时,往往会遇到各种警告。下面,我们来一起讨论:作为JPA的经典实现—Hibernate,是如何避免各种常见的错误,并充分利用Kotlin的。

JPA实体的各项规则

注意,此处的实体并非常规的数据传输对象(Data Transfer Object,DTO)。为了能够顺畅运行,实体需要得到正确地定义。链接中详细阐述了针对JPA的一系列规范和限制。其中最重要的是如下两项:

  • 实体类虽然可以拥有其他构造函数,但是必须具有一个无参数(no-arg)的构造函数。而且这个无参数构造函数必须是公共的(public)或受保护的(protected)。
  • 实体类的任何方法、或持久性实例变量,都不能为final类型。

上述规范足以让实体类运行起来。不过为了使之更流畅地运行,我们需要附加如下两条规则:

  • 只有在明确的请求时,所有Lasy关联才能被加载。否则,我们可能会遇到LazyInitializationException、或各种意外的性能问题。
  • equals()和hashCode()的实现,必须考虑到实体的可变特性。

无参数的构造函数

主构造函数(Primary constructors)是Kotlin最受欢迎的特性之一。然而,主构造函数在被加入的同时,替换了原有的默认函数。因此,如果您在Hibernate中使用它,则可能会碰到诸如:org.hibernate.InstantiationException: No default constructor for entity的异常。

那么为了解决此问题,您可以在所有实体中,手动定义无参数的构造函数。同时,您最好使用kotlin-jpa编译器插件,来确保在字节码中,为每个JPA定义相关的类,如:@Entity、@MappedSuperclass或@Embeddable,生成无参数的构造函数。

若想启用该插件,您只需将其添加到kotlin-maven-plugin和compilerPlugins的依赖关系中即可,请参见如下代码段:

  1. <plugin> 
  2.    <groupId>org.jetbrains.kotlin</groupId> 
  3.    <artifactId>kotlin-maven-plugin</artifactId> 
  4.    <configuration> 
  5.        <compilerPlugins> 
  6.            ... 
  7.            <plugin>jpa</plugin> 
  8.            ... 
  9.        </compilerPlugins> 
  10.    </configuration> 
  11.    <dependencies> 
  12.        ... 
  13.        <dependency> 
  14.            <groupId>org.jetbrains.kotlin</groupId> 
  15.            <artifactId>kotlin-maven-noarg</artifactId> 
  16.            <version>${kotlin.version}</version> 
  17.        </dependency> 
  18.        ... 
  19.    </dependencies> 
  20. </plugin> 

与之对应的在Gradle(译者注:一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具)中的代码段为:

  1. buildscript { 
  2.     dependencies { 
  3.         classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" 
  4.     } 
  5. apply plugin: "kotlin-jpa" 

打开各种类和属性

根据JPA规范,所有与JPA相关的类和属性都必须是open的。不过,某些JPA提供程序可能不会强制执行该规范。例如,Hibernate在遇到最终实体类时,是不会抛出异常的。然而,由于final类无法被子类化(subclassed),因此Hibernate的代理机制会就此关闭。而没有了代理,又何谈lazy加载呢?而且,由于程序急需获取所有的ToOne关联,因此它很可能会导致严重的性能问题。

不过,对于使用静态编织(static weaving)的EclipseLink而言,情况则不同,毕竟它的lazy加载机制并不会用到子类化。

如下代码段所示,与Java不同的是,在Kotlin中,所有的类、属性、以及方法,默认都是final类型的。您必须将它们明确地标记为open:

  1. @Table(name = "project"
  2. @Entity 
  3. open class Project { 
  4.     @Id 
  5.     @GeneratedValue(strategy = GenerationType.IDENTITY) 
  6.     @Column(name = "id", nullable = false
  7.     open var id: Long? = null 
  8.     @Column(name = "name", nullable = false
  9.     open var name: String? = null 
  10.     ... 

或者如下面的代码段所示,您最好使用全开放(all-open)式的编译器插件(https://kotlinlang.org/docs/all-open-plugin.html),来默认开启所有与JPA相关的类和属性。通过正确的配置,它能够适用于所有被注释为@Entity、 @MappedSuperclass、以及@Embeddable的类:

  1. <plugin> 
  2.    <groupId>org.jetbrains.kotlin</groupId> 
  3.    <artifactId>kotlin-maven-plugin</artifactId> 
  4.    <configuration> 
  5.        <compilerPlugins> 
  6.            ... 
  7.            <plugin>all-open</plugin> 
  8.        </compilerPlugins> 
  9.        <pluginOptions> 
  10.            <option>all-open:annotation=javax.persistence.Entity</option
  11.            <option>all-open:annotation=javax.persistence.MappedSuperclass</option
  12.            <option>all-open:annotation=javax.persistence.Embeddable</option
  13.        </pluginOptions> 
  14.    </configuration> 
  15.    <dependencies> 
  16.        <dependency> 
  17.            <groupId>org.jetbrains.kotlin</groupId> 
  18.            <artifactId>kotlin-maven-allopen</artifactId> 
  19.            <version>${kotlin.version}</version> 
  20.        </dependency> 
  21.    </dependencies> 
  22. </plugin> 

与之对应的在Gradle中的代码段为:

  1. buildscript { 
  2.     dependencies { 
  3.         classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" 
  4.     } 
  5. apply plugin: "kotlin-allopen" 
  6. allOpen { 
  7.     annotations("javax.persistence.Entity""javax.persistence.MappedSuperclass""javax.persistence.Embedabble"

针对JPA实体使用各种数据类

数据类(Data classes)是专为DTO设计的一项超棒的Kotlin功能。它被默认设计、并配备了各种非常实用的针对equals()、hashCode()、以及toString()的实现。不过,此类实现并不太适合JPA实体。其原因在于,虽然数据类被设计为final类,但是它不能够像Kotlin那样被标记为open。因此,为了适用于实体,而将它们标记为open的唯一方法便是,启用全开放式的编译器插件。

如下代码段所示,我们将使用以下实体,来进一步检查数据类。它带有一个已生成的id、一个name属性、以及两个lazy的OneToMany关联:

  1. @Table(name = "client"
  2. @Entity 
  3. data class Client( 
  4.    @Id 
  5.    @GeneratedValue(strategy = GenerationType.IDENTITY) 
  6.    @Column(name = "id", nullable = false
  7.    var id: Long? = null
  8.    @Column(name = "name", nullable = false
  9.    var name: String? = null
  10.    @OneToMany(mappedBy = "client", orphanRemoval = true
  11.    var projects: MutableSet<Project> = mutableSetOf(), 
  12.    @JoinColumn(name = "client_id"
  13.    @OneToMany 
  14.    var contacts: MutableSet<Contact> = mutableSetOf(), 

意外获取LAZY关联

默认情况下,所有ToMany关联都是lazy的,其原因在于:非必要地加载它们,往往很容易会影响到程序性能。例如,equals()、hashCode()、以及toString()在实现的过程中,通常会用到包括lazy在内的所有属性。因此,调用它们会导致向数据库产生不需要的请求、以及出现LazyInitializationException。而且,数据类的默认行为是:在其方法中,使用来自主构造函数的所有字段。

在此,我们可以使用IDE来生成toString(),以通过简单的覆盖方式,排除所有的LAZY字段。如下代码段所示,由于JPA Buddy有着自己的toString()产生机制,因此它完全不会提供LAZY字段。

  1. @Override 
  2. override fun toString(): String { 
  3.    return this::class.simpleName + "(id = $id , name = $name )" 

当然,仅从equals()和hashCode()中排除LAZY字段是远远不够的,毕竟它们可能仍然包含着可变的属性。

Equals()和HashCode()的问题

由于JPA实体在本质上是可变的,因此为其实现equals()和hashCode(),并不像常规的DTO那么简单。某些实体的id甚至都是由数据库所生成的,因此id会在实体首次被持久化后发生变化。这就意味着我们将没有可依赖的字段,去计算hashCode。

下面,让我们对Client实体进行一个简单的测试。

  1. val awesomeClient = Client(name = "Awesome client"
  2. val hashSet = hashSetOf(awesomeClient) 
  3. clientRepository.save(awesomeClient) 
  4. assertTrue(awesomeClient in hashSet) 

如上面的代码段所说,即便该实体被添加到前面几行的集合中,它的最后一行断言也会出现错误。毕竟,id在被首次生成时,hashCode就会发生改变。这就导致了HashSet在不同的存储桶中是无法查找到该实体的。可见,如果id是在实体对象的创建期间被设置的(例如,是由应用程序设置的UUID),那么就不会出现问题;而如果是由数据库生成的id(其实更为常见),就会出现上述问题。

对此,我们可以在使用实体的数据类时,持续性地覆盖equals()和hashCode()。如果您想详细地了解具体使用方法,请参见--https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/。其中,对于Client实体而言,其对应的代码段为:

  1. override fun equals(other: Any?): Boolean { 
  2.    if (this === other) return true 
  3.    if (other == null ||Hibernate.getClass(this) !=Hibernate.getClass(other)) return false 
  4.    other as Client 
  5.    return id != null && id == other.id 
  6. override fun hashCode(): Int = 1756406093 

使用由应用程序设置的ID

其实,数据类的各种方法主要是由主构造函数中那些指定的字段所生成的。如果只包含了eager immutable字段,那么数据类就不会存在上述问题。如下代码段展示了由应用程序设置的不可变id的字段:

  1. @Table(name = "contact"
  2. @Entity 
  3. data class Contact( 
  4.    @Id 
  5.    @Column(name = "id", nullable = false
  6.    val id: UUID, 
  7. ) { 
  8.    @Column(name = "email", nullable = false
  9.    val email: String? = null 
  10.    // other properties omitted 

如果您更喜欢使用由数据库来生成id的话,则可以参照如下代码段,以实现在构造函数中使用不可变的自然id:

  1. @Table(name = "contact"
  2. @Entity 
  3. data class Contact( 
  4.    @NaturalId 
  5.    @Column(name = "email", nullable = false, updatable = false
  6.    val email: String 
  7. ) { 
  8.    @Id 
  9.    @GeneratedValue(strategy = GenerationType.IDENTITY) 
  10.    @Column(name = "id", nullable = false
  11.    var id: Long? = null 
  12.    
  13.    // other properties omitted 

虽然您可以放心地使用上述方法,但是它几乎违背了使用数据类的初衷。毕竟,该用法不但让分解(decomposition)变得无效,而且让toString()对于实体而言,还不如直接使用普通的旧类。

空指针安全性(null-safety)

Kotlin相对于Java的一项优势便是,内置有空指针安全性的功能。我们可以通过非空约束(non-null constraints)在数据库端确保空指针的安全性。其最简单的实现方法为,在主构造函数中使用非空类型,来定义各种非空的属性(请参考如下代码段):

  1. @Table(name"contact"
  2. @Entity 
  3. class Contact( 
  4.    @NaturalId 
  5.    @Column(name = "email", nullable = false, updatable = false
  6.    val email: String, 
  7.    @Column(name = "name", nullable = false
  8.    var name: String 
  9.    @ManyToOne(fetch = FetchType.LAZY, optional = false
  10.    @JoinColumn(name = "client_id", nullable = false
  11.    var client: Client 
  12. ) { 
  13.    // id and other properties omitted 

当然,如果您需要从构造函数中(例如:在数据类中)排除它们,则可以提供默认值,或将lateinit的修饰符添加到其属性之中(请参考如下代码段):

  1. @Entity 
  2. data class Contact( 
  3.    @NaturalId 
  4.    @Column(name = "email", nullable = false, updatable = false
  5.    val email: String, 
  6. ) { 
  7.    @Column(name = "name", nullable = false
  8.    var name: String = "" 
  9.    @ManyToOne(fetch = FetchType.LAZY, optional = false
  10.    @JoinColumn(name = "client_id", nullable = false
  11.    lateinit var client: Client 
  12.    // id and other properties omitted 

据此,如果该属性在数据库中被确认为非空,那么我们便可以省略在Kotlin代码中,对于所有空值的检查。

小结

让我们通过如下列表,一起来总结一下如何在Kotlin中定义JPA实体:

  • 请将所有与JPA相关的类、及其属性标记为open,以避免出现显著的性能问题。
  • 为ManytoOne和OnetoOne关联开启lazy加载,或者将全开放式的编译器插件,应用到所有被注释为@Entity、@MappedSuperclass、以及@Embeddable的类。
  • 为了避免出现InstantiationException,请在所有与JPA相关的类中,定义无参数的构造函数,或使用kotlin-jpa编译器插件。
  • 通过启用全开放式的插件,在已编译的字节码中创建数据类,并使之具有open属性。
  • 覆盖equals()、hashCode()、以及toString()。
  • 让JPA Buddy生成诸如:equals()、hashCode()、以及toString()等有效的实体。

此外,如果您想深入研究此类实践,请通过链接,参考我们在GitHub存储库中为您准备的带有测试的示例。

原文标题:Best Practices of Using JPA(Hibernate) With Kotlin,作者:Andrey Oganesyan

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

责任编辑:华轩 来源: 51CTO
相关推荐

2009-06-19 18:36:15

JPAHibernate

2022-04-28 08:05:05

数据库数据库交互

2009-06-02 11:25:22

HibernateJPA映射

2023-10-07 16:20:34

JavaAPI

2022-08-19 09:01:59

ReactTS类型

2022-03-05 23:08:14

开源软件开发技术

2024-06-12 13:57:00

2021-10-06 23:31:45

HibernateJPASpring Data

2022-06-27 10:05:09

物联网安全UPS网络安全

2010-07-12 17:12:37

JPA 2.0缓存Hibernate缓存Java EE

2021-04-15 08:08:48

微前端Web开发

2019-11-27 10:55:36

云迁移云计算云平台

2019-09-17 09:44:45

DockerHTMLPython

2010-07-13 16:20:18

JPA 2.0缓存Hibernate缓存Java EE

2023-08-10 17:40:33

人工智能

2022-04-07 09:30:00

自动化LinodeKubernetes

2019-01-17 10:25:56

Python编程语言程序员

2022-01-06 09:55:19

鸿蒙HarmonyOS应用

2021-07-06 14:17:16

MLOps机器学习AI

2021-08-17 15:00:10

BEC攻击网络攻击邮件安全
点赞
收藏

51CTO技术栈公众号