博客 / 詳情

返回

Spring Data JPA 學習筆記(01)

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.