赞
踩
Kotlin 很棒:它比 Java 更简洁和富有表现力,它允许更安全的代码并提供与 Java 的无缝互操作性。 后者允许开发人员将他们的项目迁移到 Kotlin,而无需重写整个代码库。 这种迁移是我们可能不得不在 Kotlin 中使用 JPA 的原因之一。 为新的 Kotlin 应用程序选择 JPA 也很有意义,因为它是开发人员熟悉的成熟技术。
没有实体就没有 JPA,在 Kotlin 中定义它们会带来一些警告。 让我们看看如何避免常见的陷阱并充分利用 Kotlin。 剧透警告:数据类不是实体类的最佳选择。
本文将主要关注 Hibernate,因为它是所有 JPA 实现中无可置疑的领导者。
实体不是常规的 DTO。 为了工作,并且工作得好,它们需要满足某些要求,让我们从定义它们开始。 JPA 规范提供了自己的一组限制,以下是对我们最重要的两个:
主构造函数是Kotlin中最受欢迎的特性之一。但是,添加主构造函数,会使我们丢失默认的构造函数。因此,如果你尝试将其与Hibernate一起使用,你会得到以下异常:org.hibernate.InstantiationException: No default constructor for entity .
要解决这个问题,你可以在所有实体中手动添加无参构造函数。或者,最后使用kotlin-jpa编译插件,它确保在每个jpa相关类的字节码中生成无参构造函数:@Entity,@MappedSuperclass或者Embeddable
要启用该插件,只需将其添加到 kotlin-maven-plugin 的依赖项和 compilerPlugins 中:
<plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <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>
在Gradle中:
plugins {
id "org.jetbrains.kotlin.plugin.jpa" version "1.5.21"
}
根据JPA规范,所有与JPA相关的类和属性都必须是open的。一些JPA提供者不会强制执行词规则。例如,Hibernate在遇到final类时,不会抛出异常。但是,一个final类无法被继承(子类化)。因此,Hibernate的代理机制关闭。没有代理,就没有延迟加载。实际上,这意味着所有的ToOne关联总是会被立即加载。这可能导致严重的性能问题。这个情况和使用静态编织的Eclipse Link不同,因为他没有使用子类化来进行其延迟加载机制。
与 Java 不同的是,在 Kotlin 中,所有的类、属性和方法默认都是 final 的。 您必须明确将它们标记为打开:
@Table(name = "project")
@Entity
open class Project {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
open var id: Long? = null
@Column(name = "name", nullable = false)
open var name: String? = null
...
}
或者,您可以使用all-open编译器插件来使所有与 JPA 相关的类和属性默认是open的。 确保正确配置它,使其适用于所有注释为@Entity、@MappedSuperclass、@Embeddable 的类:
<plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <compilerPlugins> ... <plugin>all-open</plugin> </compilerPlugins> <pluginOptions> <option>all-open:annotation=javax.persistence.Entity</option> <option>all-open:annotation=javax.persistence.MappedSuperclass</option> <option>all-open:annotation=javax.persistence.Embeddable</option> </pluginOptions> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${kotlin.version}</version> </dependency> </dependencies> </plugin>
在Gradle中:
plugins {
id "org.jetbrains.kotlin.plugin.allopen" version "1.5.21"
}
allOpen {
annotations("javax.persistence.Entity", "javax.persistence.MappedSuperclass", "javax.persistence.Embedabble")
}
data class是专门为Dto设计的一个很棒的功能。data class被设计成final的,并且带有默认的equals方法,hashCode方法和toString方法,这些方法非常有用。但是,这些实现并不适合JPA实体,让我们看看为什么。
首先,data class是被设计成final的,因此不能被标记为open的。因此,唯一的办法就是使他们open,是启用全开放编译器插件。
为了进一步检查data class,我们将使用下面的实体,它有一个生成的id, 一个name属性和两个OneToMany的懒加载。
@Table(name = "client") @Entity data class Client( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) var id: Long? = null, @Column(name = "name", nullable = false) var name: String? = null, @OneToMany(mappedBy = "client", orphanRemoval = true) var projects: MutableSet<Project> = mutableSetOf(), @JoinColumn(name = "client_id") @OneToMany var contacts: MutableSet<Contact> = mutableSetOf(), )
默认情况下,所以ToMany的关联都是lazy:不必要地加载他们很容易损坏性能。可能发生这种情况的一个常见情况是当equals(),hashCode()和toString()实现使用所有属性时,包括lazy属性。因此,调用它们会导致对DB的不需要的请求或LazyInitializationException。这是data class的默认行为:主构造函数中的所有字段都在这些方法中使用。
toString()可以简单的被override以排除所有的lazy字段。确保在使用IDE生成的toString()时不要意外添加它们。JPA Buddy 有自己的 toString() 生成,它完全不提供 LAZY 字段作为选项。
@Override
override fun toString(): String {
return this::class.simpleName + "(id = $id , name = $name )"
}
从 equals() 和 hashCode() 中排除 LAZY 字段是不够的,因为它们可能仍然包含可变属性。
JPA 实体本质上是可变的,因此为它们实现 equals() 和 hashCode() 并不像常规 DTO 那样简单。 甚至实体的 id 也经常由数据库生成,因此在实体首次持久化后它会发生变化。 这意味着我们没有可以依赖的字段来计算 hashCode。
让我们写一个Client实体的简单测试:
val awesomeClient = Client(name = "Awesome client")
val hashSet = hashSetOf(awesomeClient)
clientRepository.save(awesomeClient)
assertTrue(awesomeClient in hashSet)
最后一行的断言失败,尽管通过上面几行代码,把实体被添加到set中。一旦生成了Id(在第一次保存时),hashCode就会改变。所以这个hashSet在不同的bucket中找这个entity,因此也找不到。如果id是在实体创建时被设置的(如id是个uuid,有app设置的)将不会有问题。但是更常见的是id是有数据库来产生的。
为了解决这个问题,在使用data class时,请始终重写equals方法和hashCode方法。Vlad Mihalcea 和 Thorben Janssen 详细解释了如何做到这一点。 对于客户端实体,它应该如下所示:
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
other as Client
return id != null && id == other.id
}
override fun hashCode(): Int = 1756406093
使用主构造函数中指定的字段生成data class中的方法。如果它只包含急切的不可变字段,则数据类不存在上述问题。此类字段的一个示例是应用程序设置的不可变id:
@Table(name = "contact")
@Entity
data class Contact(
@Id
@Column(name = "id", nullable = false)
val id: UUID,
) {
@Column(name = "email", nullable = false)
val email: String? = null
// other properties omitted
}
如果你更愿意使用数据库生成id,一个不可变的id可以再构造器中使用:
@Table(name = "contact")
@Entity
data class Contact(
@NaturalId
@Column(name = "email", nullable = false, updatable = false)
val email: String
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
var id: Long? = null
// other properties omitted
}
这是绝对安全的使用。 然而,它几乎违背了使用数据类的目的,因为它使分解毫无用处,并且只使用 toString() 中的一个字段。 一个普通的旧类可能是实体的更好选择。
Kotlin 相对于 Java 的优势之一是内置的 null 安全特性。 也可以通过非空约束在 DB 端确保空安全。 只有将这些功能一起使用才有意义。
最简单的方法是使用非空类型在主构造函数中定义非空属性:
@Table(name = "contact") @Entity class Contact( @NaturalId @Column(name = "email", nullable = false, updatable = false) val email: String, @Column(name = "name", nullable = false) var name: String @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "client_id", nullable = false) var client: Client ) { // id and other properties omitted }
但是,如果您需要从构造函数中排除它们(例如在数据类中),您可以提供默认值或将 lateinit 修饰符添加到属性:
@Entity
data class Contact(
@NaturalId
@Column(name = "email", nullable = false, updatable = false)
val email: String,
) {
@Column(name = "name", nullable = false)
var name: String = ""
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "client_id", nullable = false)
lateinit var client: Client
// id and other properties omitted
}
因此,如果该属性在 DB 中确定不为 null,我们也可以省略 Kotlin 代码中的所有 null 检查。
您可以在我们的 GitHub 存储库中找到更多带有测试的示例。 作为如何在 Kotlin 中定义 JPA 实体的总结,这里有一个清单:
使用data class
JPA Buddy 知道所有这些事情,并且总是为您生成有效的实体,包括额外的东西,如 equals()、hashCode()、toString()。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。