在国内,使用 JPA 的开发者相对较少,甚至出现了 Hibernate 将被 MyBatis 替代的说法。各大技术社区中,“Hibernate 已死”的论调也不鲜见。然而在欧美地区,类似 MyBatis 这样的 SQL 模板工具几乎没有市场。同时,关于 JPA 的中文资料也较为匮乏,网上能找到的教程大多浅尝辄止,一旦遇到稍复杂的需求便难寻解决方案。
我使用 JPA 已有多年,也看过许多人对它的吐槽。本文将结合我遇到的实际问题以及在各大论坛看到的常见疑问,做一个总结,希望能帮助大家更客观地看待 JPA 这一技术。
1. 简介
1.1 JPA 和 Hibernate 的关系
JPA(Java Persistence API)是 Java 官方的 ORM 标准,而 Hibernate 是 JPA 规范的一个实现。它们之间的关系类似于 Servlet 规范与 Tomcat 容器,或者 JDBC 接口与 MySQL Connector/J 驱动。
JPA 主要定义了以下内容:
- 如何用注解(如
@Entity, @Id, @OneToMany)描述对象与数据库表的映射关系。
- 如何通过
EntityManager 接口进行增删改查操作。
- JPQL(Java Persistence Query Language)的语法。这是一种面向对象的查询语言,其操作对象是实体,而非直接操作数据库表。
除了 Hibernate,还有其他 JPA 实现,例如 EclipseLink 和 OpenJPA。但在实现成熟度上,Hibernate 更胜一筹,并且也是 Spring 框架默认集成的 JPA 实现。
1.2. 使用 JPA 的优势
- 完整的 ORM:实现了对象与关系数据库的映射,支持继承、关联、生命周期回调等面向对象特性。
- 类型安全与 IDE 支持:基于实体对象和接口方法进行查询,避免了字符串拼接,编译期即可发现错误,IDE 也能提供更好的自动补全和重构支持。
- 数据库方言透明:通过配置即可切换不同的数据库,通常无需修改 SQL 或 XML 映射文件(某些复杂场景下可能失效)。
- Spring 官方强力支持:对于广大 Java 开发者而言,Spring 生态的选择值得信赖,这为JPA的普及和应用提供了坚实基础。
- 对 GraalVM Native 的完美支持:在 Spring 框架体系下,若希望应用转换为原生镜像以获得极致启动速度和内存占用,选择 JPA 通常更为顺畅。
1.3. 使用 JPA 的劣势
- 学习成本较高:注解体系庞杂,且从数据库表反向生成实体代码的质量往往不佳。建议始终坚持从代码模型生成数据库表结构。
- 复杂查询编写有挑战:对于非常复杂的嵌套关联查询,JPQL 或 Criteria API 可能显得有些繁琐,有时需要借助 QueryDSL 等第三方工具。
2. 起步
下面我们通过一个完整的 User(用户)和 Role(角色)模型示例,来构建一个简单的 JPA 工程。
2.1 依赖引入
本例基于 Spring Boot 4.0,部分依赖写法与 Spring Boot 3.x 可能有所不同。
在 Spring Boot 项目中引入 Spring Data JPA 非常简单,只需添加对应的 starter 依赖即可。
dependencies {
// 添加spring boot data jpa依赖
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// 添加h2数据库控制台依赖
implementation("org.springframework.boot:spring-boot-h2console")
// 添加h2数据库依赖
runtimeOnly("com.h2database:h2")
}
其他依赖说明:
spring-boot-h2console:本示例使用 H2 作为内存数据库,此依赖提供了一个网页控制台用于查看和管理 H2 数据库。
com.h2database:h2:H2 数据库的 JDBC 驱动。
2.2 添加配置文件
在 application.yml 中加入相关配置。
spring:
datasource:
url: "jdbc:h2:~/test"
username: sa
password: ""
jpa:
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true
基于以上配置,可以实现如下效果:
- 项目启动时,连接到本地的 H2 数据库,数据库文件将持久化在
~/test.mv.db 中。
- 数据库连接用户名为
sa,密码为空。
- 项目启动时,会根据实体类的定义,自动更新数据库表结构。
- 程序运行时,会在控制台打印出执行的 SQL 语句。
2.3 构建 User 模型
在 model 包中,构建与数据库表对应的 User 实体类。
@Entity
@Table(name = "t_user_")
class User {
@Id
@Column(name = "id_")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
@Column(name = "username_", length = 50)
var username: String = ""
@Column(name = "password_", length = 64)
var password: String = ""
@Column(name = "created_time_")
val cratedTime: ZonedDateTime = ZonedDateTime.now()
@Column(name = "last_modified_time_")
var lastModifiedTime: ZonedDateTime = ZonedDateTime.now()
}
这个模型就是一个添加了 JPA 注解的普通 Kotlin 数据类。这些注解来自 jakarta.persistence 包,是 JPA 的核心:
@Entity 必需,标识该类是一个 JPA 实体。
@Id 必需,标识该属性对应数据库表的主键。
@Table 可选,用于配置实体对应的数据库表名、索引等信息。
@Column 可选,用于配置属性对应的数据库字段名、长度、精度等。
@GeneratedValue 可选,标识主键的生成策略(如自增)。
我们不需要在 @Column 中显式指定数据库字段类型,JPA 提供商会自动完成 Java/Kotlin 基础类型到数据库类型的映射。
2.4 创建 Repository 接口
接下来,创建一个接口并继承 JpaRepository。之后便可以在项目中注入这个接口的实例,并使用其内置方法实现基本的增删改查。
interface UserRepository : JpaRepository<User, Long>
2.5 创建相应的 Service
我们通常在 Service 层定义业务方法,并在此注入 Repository 来实现业务逻辑。
interface UserService {
fun save(request: UserRequest): UserDto
fun update(id: Long, request: UserRequest): UserDto
fun delete(id: Long)
fun findById(id: Long): UserDto?
fun find(pageable: Pageable): PagedModel<UserDto>
}
@Service
class UserServiceImpl(
private val userRepository: UserRepository
) : UserService {
@Transactional
override fun save(request: UserRequest): UserDto {
val user = User().apply {
username = request.username
password = request.password
lastModifiedTime = ZonedDateTime.now()
}
val savedUser = userRepository.save(user)
return UserDto(savedUser)
}
@Transactional
override fun update(id: Long, request: UserRequest): UserDto {
val user = userRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("用户不存在")
user.apply {
username = request.username
password = request.password
lastModifiedTime = ZonedDateTime.now()
}
val updatedUser = userRepository.save(user)
return UserDto(updatedUser)
}
@Transactional
override fun delete(id: Long) {
userRepository.deleteById(id)
}
@Transactional(readOnly = true)
override fun findById(id: Long): UserDto? {
return userRepository.findByIdOrNull(id)?.let { UserDto(it) }
}
@Transactional(readOnly = true)
override fun find(pageable: Pageable): PagedModel<UserDto> {
return userRepository.findAll(pageable).map {
UserDto.Companion(it)
}.let {
PagedModel(it)
}
}
}
整体逻辑非常清晰:构建实体对象、赋值、调用 Repository 的 save、findById、deleteById 等方法完成持久化操作。
最后,在 Controller 中调用 Service 即可提供 RESTful API。
@RestController
@RequestMapping("/users")
class UserController(
private val userService: UserService
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun save(@RequestBody request: UserRequest): UserDto {
return userService.save(request)
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.CREATED)
fun update(@PathVariable id: Long, @RequestBody request: UserRequest): UserDto {
return userService.update(id, request)
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun delete(@PathVariable id: Long) {
userService.delete(id)
}
@GetMapping("/{id}")
fun findById(@PathVariable id: Long): UserDto? {
return userService.findById(id)
}
@GetMapping
fun find(
pageable: Pageable
): PagedModel<UserDto> {
return userService.find(pageable)
}
}
本文演示了 Spring Data JPA 结合 Spring Boot 4.0 的快速入门。后续将持续更新 JPA 的更多进阶应用,例如复杂关联映射、动态查询、性能优化等内容。完整的示例项目代码可在 https://github.com/ldwqh0/jpa-demo.git 获取。欢迎在云栈社区与其他开发者交流探讨 JPA 的使用心得与最佳实践。