1. 简介
Kotlin 的一大特性是它与 Java 生态的无缝互操作性,而 JPA(Java Persistence API)作为 Java 领域中最常用的 ORM 标准之一,自然也成为了 Kotlin 开发中经常打交道的部分。
本文将带你了解如何在 Kotlin 中使用类作为 JPA 实体,以及需要注意的一些坑。
2. 依赖配置
为了演示方便,我们使用 Hibernate 作为 JPA 的实现框架。在 Maven 项目中添加以下依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.2.15.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-testing</artifactId>
<version>5.2.15.Final</version>
<scope>test</scope>
</dependency>
同时,使用 H2 嵌入式数据库进行测试:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
<scope>test</scope>
</dependency>
Kotlin 相关依赖:
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.8.0</version>
</dependency>
⚠️ 注意:以上版本仅供参考,实际开发中请使用 Maven Central 上最新的版本。
3. 编译器插件:jpa-plugin
✅ JPA 要求实体类必须拥有一个无参构造函数。
然而,Kotlin 类默认是没有无参构造函数的,这就需要借助 Kotlin 的 jpa-plugin 插件来生成:
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>1.8.0</version>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>jpa</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
<!--...-->
</plugin>
这个插件会为标注了 @Entity
的类自动生成无参构造函数,让 Hibernate 能够正常实例化实体对象。
4. 在 Kotlin 类中使用 JPA
配置完成后,我们就可以愉快地使用 Kotlin 类作为 JPA 实体了。
先来定义一个简单的 Person
类:
@Entity
class Person(
@Column(nullable = false)
val name: String,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int?=null,
)
✅ 你可以像在 Java 中一样使用 JPA 注解,比如 @Entity
、@Column
、@Id
等。
⚠️ 注意:建议将 id
字段放在最后,因为它是可空的(由数据库自动生成),放在构造函数最后更符合 Kotlin 的习惯。
写个测试验证一下:
@Test
fun givenPerson_whenSaved_thenFound() {
doInHibernate(({ this.sessionFactory() }), { session ->
val personToSave = Person("John")
session.persist(personToSave)
val personFound = session.find(Person::class.java, personToSave.id)
session.refresh(personFound)
assertTrue(personToSave.name == personFound.name)
})
}
运行后你会看到 Hibernate 输出的 SQL:
Hibernate: insert into Person (id, name) values (null, ?)
Hibernate: select person0_.id as id1_0_0_, person0_.name as name2_0_0_ from Person person0_ where person0_.id=?
✅ 一切正常。
❌ 如果没有启用 jpa-plugin
,你会遇到如下异常:
javax.persistence.PersistenceException: org.hibernate.InstantiationException: No default constructor for entity: : com.baeldung.entity.Person
4.1. 处理可空字段和关联关系
我们再扩展一下 Person
类,添加可空字段和一对多关系:
//...
@Column(nullable = true)
val email: String? = null,
@Column(nullable = true)
@OneToMany(cascade = [CascadeType.ALL])
val phoneNumbers: List<PhoneNumber>? = null
对应的 PhoneNumber
类如下:
@Entity
class PhoneNumber(
@Column(nullable = false)
val number: String,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Int?=null,
)
测试一下带空值的情况:
@Test
fun givenPersonWithNullFields_whenSaved_thenFound() {
doInHibernate(({ this.sessionFactory() }), { session ->
val personToSave = Person("John", null, null)
session.persist(personToSave)
val personFound = session.find(Person::class.java, personToSave.id)
session.refresh(personFound)
assertTrue(personToSave.name == personFound.name)
})
}
输出 SQL:
Hibernate: insert into Person (id, email, name) values (null, ?, ?)
Hibernate: select person0_.id as id1_0_1_, person0_.email as email2_0_1_, person0_.name as name3_0_1_, phonenumbe1_.Person_id as Person_i1_1_3_, phonenumbe2_.id as phoneNum2_1_3_, phonenumbe2_.id as id1_2_0_, phonenumbe2_.number as number2_2_0_ from Person person0_ left outer join Person_PhoneNumber phonenumbe1_ on person0_.id=phonenumbe1_.Person_id left outer join PhoneNumber phonenumbe2_ on phonenumbe1_.phoneNumbers_id=phonenumbe2_.id where person0_.id=?
再测试一下带完整数据的情况:
@Test
fun givenPersonWithFullData_whenSaved_thenFound() {
doInHibernate(({ this.sessionFactory() }), { session ->
val personToSave = Person(
"John",
"[email protected]",
Arrays.asList(PhoneNumber("202-555-0171"), PhoneNumber("202-555-0102")))
session.persist(personToSave)
val personFound = session.find(Person::class.java, personToSave.id)
session.refresh(personFound)
assertTrue(personToSave.name == personFound.name)
})
}
输出 SQL:
Hibernate: insert into Person (id, email, name) values (null, ?, ?)
Hibernate: insert into PhoneNumber (id, number) values (null, ?)
Hibernate: insert into PhoneNumber (id, number) values (null, ?)
✅ 三行插入语句,说明一对多关系也保存成功。
5. Kotlin Data Classes 与 JPA
Kotlin 的 Data Classes 是为数据载体设计的类,提供了默认的 equals
、hashCode
和 toString
实现。
但 ❌ 不推荐将 Data Classes 用作 JPA 实体,原因如下。
我们先看一个例子:
@Entity
data class Address(
val name: String,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
val phoneNumbers: List<PhoneNumber>,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Int? = null,
)
5.1. equals 与 hashCode 的问题
大多数 JPA 实体都有自动生成的字段(如主键),这些字段在持久化前是 null
,持久化后才赋值。
这就导致在持久化前后,equals
和 hashCode
的计算结果不同,容易引发在 Set 或 Map 中找不到对象的问题:
@Test
fun givenAddressWithDefaultEquals_whenAddedToSet_thenNotFound() {
doInHibernate({ sessionFactory() }) { session ->
val addresses = mutableSetOf<Address>()
val address = Address(name = "Berlin", phones = listOf(PhoneNumber("42")))
addresses.add(address)
assertTrue(address in addresses)
session.persist(address)
assertFalse { address in addresses }
}
}
❌ 持久化后,对象的 hashCode
变了,Set 就找不到了。
5.2. toString 可能触发懒加载
Data Classes 的 toString
是基于所有属性生成的,如果其中有懒加载的字段(如 @OneToMany
),调用 toString
会触发额外查询:
@Test
fun givenAddress_whenLogging_thenFetchesLazyAssociations() {
doInHibernate({ this.sessionFactory() }) { session ->
val addressToSave = Address(name = "Berlin", phoneNumbers = listOf(PhoneNumber("42")))
session.persist(addressToSave)
session.clear()
val addressFound = session.find(Address::class.java, addressToSave.id)
assertFalse { Hibernate.isInitialized(addressFound.phoneNumbers) }
logger.info("found the entity {}", addressFound) // initializes the lazy collection
assertTrue(Hibernate.isInitialized(addressFound.phoneNumbers))
}
}
输出 SQL:
Hibernate: select * from Address address0_ where address0_.id=?
Hibernate: select * from Address_PhoneNumber phonenumbe0_ inner join PhoneNumber phonenumbe1_ on phonenumbe0_.phoneNumbers_id=phonenumbe1_.id where phonenumbe0_.Address_id=?
✅ 本来只是打印日志,却触发了懒加载,影响性能。
6. 总结
本文带你体验了如何在 Kotlin 中使用 JPA 实体类,重点介绍了:
- 如何通过
jpa-plugin
解决无参构造函数问题 - 如何定义实体类和处理可空字段
- 为什么 不推荐 使用 Data Classes 作为 JPA 实体(坑多)
✅ 总之,Kotlin 和 JPA 可以愉快地合作,但要注意细节,尤其是 Data Classes 的使用场景。
💡 本文源码见:GitHub 项目地址