Spring data jpa 系列指南笔记 (一)

89 阅读5分钟

在国内,使用jpa的人可谓是少的可怜。甚至已经出现了hibernate被mybatis替代的说法。翻翻各大社区,基本都是hibernate已死的论调。但在欧美地区,压根就没有mybatis这种sql模板工具的市场。jpa中文资料极度缺乏。网上搜的各种资料也都是浅尝辄止。一旦遇到一点复杂的需求,就没有下文。

我用jpa许多年了。也看了很多人们对于jpa的吐槽。就我碰到的问题和在各大论坛遇到的问题,做个总结,希望人们少点对jpa的偏见。

1. 简介

1.1 JPA和hibernate的关系

JPA是Java 官方的 ORM 标准,Hibernate是JPA规范的一个实现。他们的关系类似于。servlet和tomcat,jdbc和MySQL Connector/J。

它定义了:

  • 如何用注解(如 @Entity, @Id, @OneToMany)描述对象与数据库表的映射;这只是一堆注解,没有解析器它就是个摆设。
  • 如何通过 EntityManager 进行增删改查;这玩意是个接口。就行jdbc的Connection一样,里面只有一堆方法定义。jpa不提供任何实现。
  • JPQL(Java Persistence Query Language)语法等。这个不是sql,他翻译过来是java持久化对象查询语言,它查询的是对象。而不是数据库。

除了hibernate之外,还有几个Jpa的实现:EclipseLink, OpenJPA。但它们在实现成熟度上都没有hibernate高,而spring默认的jpa实现就是Hibernate。

1.2. 使用jpa的优势

  • 完整 ORM(Object-Relational Mapping),将数据库表映射为 Java 对象,支持继承、关联、生命周期回调等 OOP 特性;
  • 编译期类型安全 + IDE 自动补全;代码没有文本字符串性的内容,错误,重构IDE都能自动解决。
  • 数据库方言,使用配置就可切换数据库实现,不需要改xml文件里面的特定位置(这个在某些情况下会失效)
  • Spring的官方支持,我认为这个是非常重要的。面向Spring编程的javaer们,Spring的选择值得信赖。
  • 对graalvm-native的支持完美,在Spring的框架体系下,想用native镜像,最好选择JPA,graalvm-native是质的飞跃。

1.3. 使用JPA的劣势

  • 学习成本高,注解确实多,从数据库反向的结果质量低。建议只从代码构建数据库字段。
  • 复杂的嵌套关联查询有点难搞,需要借助第三方工具实现。

2. 起步

下面通过一个简单的例子来 User,Role模型,来实现一个jpa完整工程

2.1 依赖引入

本项目的示例基于spring boot 4.0,所有有些依赖的写法和spring boot 3.x不一致。

要在spring boot项目中使用spring data jpa,只需要添加spring boot data jpa相关的依赖即可。

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的数据库驱动

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()
}

这个模型就是一个简单的java类,在上面添加了一些注解。这些注解都位于jakarta.persistence包下,这些就是jpa的核心。它们是:

  • @Entity 必需,这是JPA实体的核心注解,它标识了这个类是一个jpa数据实体,通过它和数据库进行交互
  • @Id 必需,标识实体的主键属性。每个实体必须有一主键属性。
  • @Table 可选,它用于标识实体在数据库中的相关配置,包括名称,索引,约束。
  • @Column 可选,它标识了这个属性在数据库中对应的字段配置,包括长度,精度
  • @GeneratedValue 可选 标识这个属性在保存时通过某种方式自动生成的。我们在代码中不需要对他进行赋值
  • 更多的注解会在后面引入,有了现在这些就可以实现简单的增删改查了。

我们不需要在@Column中指定数据类型,JPA引擎会自动做好java基础类型到数据库类型的映射。

2.4 创建repository接口

接下来,创建一个接口,继承自JpaRepository,然后就可以在项目中注入这个接口,并通过它来实现增删改查

interface UserRepository : JpaRepository<User, Long>

2.5 创建相应的Service

我们一般在service中定义业务方法。在service中注入repository实现对应的业务逻辑。

  • UserService.kt
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>
}
  • UserServiceImpl.kt
@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方法将数据库持久化到数据库。

接下来就可以在controller中调用service方法了。


@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)
    }
}

后续会持续的更新其它内容。直到jpa的进阶应用,欢迎关注。 项目地址:github.com/ldwqh0/jpa-…