Spring Data JPA 学习笔记(01)

为什么需要 Spring Data JPA

Java 是一门面向对象的编程语言,数据以对象的形式组织在内存当中。如何将这些数据存储在常见的关系型数据库中是我们要解决的问题。
2001 年,一款名为 Hibernate 的 ORM 框架为我们提供了一种解决方案。ORM 即 object-relational mapping,中文翻译为对象-关系映射。该框架主要负责维护 Java 类和数据库表两者间的映射关系。Hibernate 提供了很多强大的特性和方便的功能,在众多框架中脱颖而出。

可是这跟 Spring Data JPA 有什么关系?

Java 一个很好的特性就是:对于一个常见的需求,大家坐下来讨论讨论,先把标准(接口)定下来,之后你们这些框架怎么写我不管,只要实现标准就好。该标准即 Java Persistence API,缩写为 JPA。而 Spring Data JPA 是对 JPA 提供的接口进行更进一步的封装,程序员只需要关注程序设计,不再需要耗费大量精力在框架的配置和数据的获取上。而 Hibernate 的设计十分优秀,时至今日,JPA 的默认实现仍是 Hibernate。三者关系如下:

┌─────────────────────────┐
│                         │
│     Spring Data JPA     │
│                         │
└────────────┬────────────┘
             │
             │ 封装 JPA 提供的接口
             │
             │
┌────────────▼────────────┐
│                         │
│           JPA           │
│                         │
└────────────▲────────────┘
             │
             │
             │ 实现 JPA 规范
             │
┌────────────┴────────────┐
│                         │
│        Hibernate        │
│                         │
└─────────────────────────┘
Persistence 一词中文翻译为「持续存在」,「持久化」。如何理解?

因为 Java 对象存在于内存当中,内存断电后数据就消失了,而将该对象存储在外部(如数据库中、或直接序列化写到文件中)则数据可以持久保存,所以将对象存储在外部的这个过程称之为持久化

Hello Spring Data JPA

Spring Initializr 新建 Spring Boot 项目,主要依赖配置如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

其中 H2 是一个基于内存的数据库,通常用来测试持久化功能。

application.properties 配置如下:

# 配置开启终端彩色输出
spring.output.ansi.enabled=ALWAYS

# 配置在标准输出中显示编译的 SQL 语句
logging.level.org.hibernate.SQL=DEBUG
# 配置显示执行的 SQL 语句中参数的值
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# 开启 H2 数据库的终端,这样可以查看代码对数据库的影响
spring.h2.console.enabled=true

# 配置用户名和密码,用来登录 H2 终端
spring.datasource.username=sa
spring.datasource.password=password

# 先配置,之后再讲解原因
spring.jpa.open-in-view=false

接下来配置我们要向数据库中存储的类,就叫他 Person 好了。

package com.cvuuhk.jpa;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

// @Entity 是 JPA 提供的注解,表示所修饰的类是一个实体
@Entity
public class Person {
  // @Id 是 JPA 提供的注解,表示所修饰的字段作为该实体的主键
  @Id
  // @GeneratedValue 是 JPA 提供的注解,用来自动生成主键
  @GeneratedValue
  private Long id;

  private String name;

  // JPA 规范要求实体类必须有一个至少是 protected 修饰的无参构造函数
  public Person(){}

  public Person(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return "Person{" +
            "id=" + id +
            ", name='" + name + '\'' +
            '}';
  }
}
「实体」是什么?

可以简单理解为实体就是我们要向数据库中存储的对象,就现阶段来说,一个实体类对应数据库的一张表。

有了实体类,怎么把他存放到数据库中呢?或者说,我应该调用什么方法来操作数据库呢?
我们使用 Spring Data JPA 提供的 Repository 接口。

package com.cvuuhk.jpa;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PersonRepository extends JpaRepository<Person, Long> {
}

如果你使用 IDE 提供的工具点进 JpaRepository 接口的定义中,会发现该接口定义了许多 save,find 之类的方法,这就是我们要用的。

现在我们模拟真实环境中的写法:写一个 service 类,该类实现各种业务逻辑。先写一个增加用户的吧。

package com.cvuuhk.jpa;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class PersonService {
  private final Logger logger = LoggerFactory.getLogger(PersonService.class);
  private final PersonRepository repository;

  public PersonService(PersonRepository repository) {
    this.repository = repository;
  }

  public void addPerson(String name) {
    Person person = new Person(name);
    repository.save(person);
    logger.info(person + " saved.");
  }
}

最后,我们希望程序启动时执行 addPerson 方法,新建 Startup 类配置启动时执行的指令:

package com.cvuuhk.jpa;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Startup {
  private final PersonService service;

  public Startup(PersonService service) {
    this.service = service;
  }

  @Bean
  public CommandLineRunner executeWhenStartup(){
    return args -> {
      service.addPerson("疏蒿君");
    };
  }
}

目录结构如下:

src
├── main
│   ├── java
│   │   └── com
│   │       └── cvuuhk
│   │           └── jpa
│   │               ├── Application.java
│   │               ├── Person.java
│   │               ├── PersonRepository.java
│   │               ├── PersonService.java
│   │               └── Startup.java
│   └── resources
│       └── application.properties
└── test
    └── java
        └── com
            └── cvuuhk
                └── jpa
                    └── ApplicationTests.java

OK,现在启动 Spring Boot,日志输出大致如下:

2022-08-14 13:06:58.546  INFO 681729 --- [           main] com.cvuuhk.jpa.Application               : No active profile set, falling back to 1 default profile: "default"
2022-08-14 13:06:59.104  INFO 681729 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-08-14 13:06:59.139  INFO 681729 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 29 ms. Found 1 JPA repository interfaces.
2022-08-14 13:06:59.558  INFO 681729 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-08-14 13:06:59.566  INFO 681729 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-08-14 13:06:59.566  INFO 681729 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-08-14 13:06:59.642  INFO 681729 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-08-14 13:06:59.642  INFO 681729 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1041 ms
2022-08-14 13:06:59.681  INFO 681729 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-08-14 13:06:59.839  INFO 681729 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-08-14 13:06:59.848  INFO 681729 --- [           main] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:a8c0d009-ab4b-4d5a-8d6e-69b76ea317b5'
2022-08-14 13:06:59.967  INFO 681729 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-08-14 13:07:00.000  INFO 681729 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.10.Final
2022-08-14 13:07:00.108  INFO 681729 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2022-08-14 13:07:00.190  INFO 681729 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2022-08-14 13:07:00.543 DEBUG 681729 --- [           main] org.hibernate.SQL                        : drop table if exists person CASCADE 
2022-08-14 13:07:00.546 DEBUG 681729 --- [           main] org.hibernate.SQL                        : drop sequence if exists hibernate_sequence
2022-08-14 13:07:00.548 DEBUG 681729 --- [           main] org.hibernate.SQL                        : create sequence hibernate_sequence start with 1 increment by 1
2022-08-14 13:07:00.550 DEBUG 681729 --- [           main] org.hibernate.SQL                        : create table person (id bigint not null, name varchar(255), primary key (id))
2022-08-14 13:07:00.556  INFO 681729 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-08-14 13:07:00.562  INFO 681729 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-08-14 13:07:01.101  INFO 681729 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-08-14 13:07:01.109  INFO 681729 --- [           main] com.cvuuhk.jpa.Application               : Started Application in 2.989 seconds (JVM running for 3.356)
2022-08-14 13:07:01.124 DEBUG 681729 --- [           main] org.hibernate.SQL                        : call next value for hibernate_sequence
2022-08-14 13:07:01.161 DEBUG 681729 --- [           main] org.hibernate.SQL                        : insert into person (name, id) values (?, ?)
2022-08-14 13:07:01.163 TRACE 681729 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [疏蒿君]
2022-08-14 13:07:01.164 TRACE 681729 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2022-08-14 13:07:01.168  INFO 681729 --- [           main] com.cvuuhk.jpa.PersonService             : Person{id=1, name='疏蒿君'} saved.

我将其中比较重要的信息摘出来:

2022-08-14 13:06:59.558  INFO 681729 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-08-14 13:06:59.848  INFO 681729 --- [           main] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:a8c0d009-ab4b-4d5a-8d6e-69b76ea317b5'
2022-08-14 13:07:00.548 DEBUG 681729 --- [           main] org.hibernate.SQL                        : create sequence hibernate_sequence start with 1 increment by 1
2022-08-14 13:07:00.550 DEBUG 681729 --- [           main] org.hibernate.SQL                        : create table person (id bigint not null, name varchar(255), primary key (id))
2022-08-14 13:07:01.124 DEBUG 681729 --- [           main] org.hibernate.SQL                        : call next value for hibernate_sequence
2022-08-14 13:07:01.161 DEBUG 681729 --- [           main] org.hibernate.SQL                        : insert into person (name, id) values (?, ?)
2022-08-14 13:07:01.163 TRACE 681729 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [疏蒿君]
2022-08-14 13:07:01.164 TRACE 681729 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2022-08-14 13:07:01.168  INFO 681729 --- [           main] com.cvuuhk.jpa.PersonService             : Person{id=1, name='疏蒿君'} saved.

根据这些日志,我们得到以下信息:

  1. 服务端口为 8080
  2. H2 数据库开了一个终端,地址为 jdbc:h2:mem:a8c0d009-ab4b-4d5a-8d6e-69b76ea317b5
  3. 创建了一个名为 hibernate_sequence 的序列,这个序列干什么用的?我们之前在实体类中配置了自动生成主键,主键的值就是根据这个序列来生成的,能看到该序列起始值为 1,递增值为 1
  4. 创建了一个名为 person 的数据库表
  5. 从序列 hibernate_sequence 中获取 id 的值
  6. 向数据库表 person 中插入值,其中 id 为 1,name 为 疏蒿君
  7. 根据代码,最后一行日志表示 addPerson 方法执行完成

接下来访问 http://localhost:8080/h2-console,将刚才日志中打印的终端地址填入 JDBC URL 输入框中,之后再填入 application.properties 文件中配置的用户名和密码。登录后执行 SELECT * FROM PERSON 得到结果如下:

+-------------+
| ID |  NAME  |
+----+--------+
| 1  | 疏蒿君 |
+ ---+--------+

我们成功地将数据存到了数据库中,其他的操作使用接口提供的其他方法即可。

Q&A

建序列,建表,插入数据的这些 SQL 语句哪来的?

Hibernate 生成的。

我的 PersonRepository 是个接口,save() 方法没实现为什么能调用?

Spring Data JPA 帮我们实现的这个接口,具体的之后再写。

结束语

本篇是我的第一篇博客,内容比较简单,一方面是尝试把自己所学所想表达出来。另一方面也是为了督促自己不断学习。之后会聚焦于内容,尝试写一些更有深度的文章。
怎么说呢……

心有所向,日复一日,必有精进。
          ——刻晴

刻晴.jpg


疏蒿君
1 声望0 粉丝

引用和评论

0 条评论