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.
根據這些日誌,我們得到以下信息:
- 服務端口為 8080
- H2 數據庫開了一個終端,地址為
jdbc:h2:mem:a8c0d009-ab4b-4d5a-8d6e-69b76ea317b5 - 創建了一個名為
hibernate_sequence的序列,這個序列幹什麼用的?我們之前在實體類中配置了自動生成主鍵,主鍵的值就是根據這個序列來生成的,能看到該序列起始值為 1,遞增值為 1 - 創建了一個名為
person的數據庫表 - 從序列
hibernate_sequence中獲取 id 的值 - 向數據庫表
person中插入值,其中 id 為1,name 為疏蒿君 - 根據代碼,最後一行日誌表示
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 幫我們實現的這個接口,具體的之後再寫。
結束語
本篇是我的第一篇博客,內容比較簡單,一方面是嘗試把自己所學所想表達出來。另一方面也是為了督促自己不斷學習。之後會聚焦於內容,嘗試寫一些更有深度的文章。
怎麼説呢……
心有所向,日復一日,必有精進。
——刻晴