使用 Spring Boot 和 JPA 儲存 PostgreSQL JSONB
1. 概述
本教學將使我們全面了解如何在 PostgreSQL JSONB
欄位中儲存 JSON 資料。
我們將快速回顧如何使用 JPA 處理儲存在可變字元 ( VARCHAR
) 資料庫列中的 JSON 值。之後,我們將比較VARCHAR
類型和JSONB
類型之間的差異,以了解JSONB
的附加功能。最後,我們將解決 JPA 中的映射JSONB
類型。
VARCHAR
映射
在本節中,我們將探討如何使用AttributeConverter
將VARCHAR
類型的 JSON 值轉換為自訂 Java POJO。
其目的是為了方便Java資料類型中的實體屬性值和資料庫列中對應的值之間的轉換。
2.1. Maven依賴
要建立AttributeConverter
,我們必須在pom.xml
中包含Spring Data JPA依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.7.18</version>
</dependency>
2.2.表定義
讓我們使用以下資料庫表定義透過一個簡單的範例來說明這個概念:
CREATE TABLE student (
student_id VARCHAR(8) PRIMARY KEY,
admit_year VARCHAR(4),
address VARCHAR(500)
);
student
表有三個字段,我們期望address
列儲存具有以下結構的 JSON 值:
{
"postCode": "TW9 2SF",
"city": "London"
}
2.3.實體類別
為了處理這個問題,我們將建立一個對應的 POJO 類別來表示 Java 中的位址資料:
public class Address {
private String postCode;
private String city;
// constructor, getters and setters
}
接下來,我們將建立一個實體類別StudentEntity,
並將其對應到我們先前建立的student
表:
@Entity
@Table(name = "student")
public class StudentEntity {
@Id
@Column(name = "student_id", length = 8)
private String id;
@Column(name = "admit_year", length = 4)
private String admitYear;
@Convert(converter = AddressAttributeConverter.class)
@Column(name = "address", length = 500)
private Address address;
// constructor, getters and setters
}
我們將使用@Convert
註釋address
字段,並應用AddressAttributeConverter
將Address
實例轉換為其 JSON 表示形式。
2.4. AttributeConverter
我們將實體類別中的address
欄位對應到資料庫中的VARCHAR
類型。但是,JPA 無法自動執行自訂 Java 類型和VARCHAR
類型之間的轉換。 AttributeConverter
透過提供處理轉換過程的機制來彌補這一差距。
我們使用AttributeConverter
將自訂 Java 資料類型儲存到資料庫列。必須為每個AttributeConverter
實作定義兩種轉換方法。一種將Java資料類型轉換為對應的資料庫資料類型,另一種將資料庫資料類型轉換為Java資料類型:
@Converter
public class AddressAttributeConverter implements AttributeConverter<Address, String> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(Address address) {
try {
return objectMapper.writeValueAsString(address);
} catch (JsonProcessingException jpe) {
log.warn("Cannot convert Address into JSON");
return null;
}
}
@Override
public Address convertToEntityAttribute(String value) {
try {
return objectMapper.readValue(value, Address.class);
} catch (JsonProcessingException e) {
log.warn("Cannot convert JSON into Address");
return null;
}
}
}
convertToDatabaseColumn()
負責將實體欄位值轉換為對應的資料庫列值,而convertToEntityAttribute()
負責將資料庫列值轉換為對應的實體欄位值。
2.5.測試用例
現在,讓我們建立一個測試案例來將Student
實例保存在資料庫中:
@Test
void whenSaveAnStudentEntityAndFindById_thenTheRecordPresentsInDb() {
String studentId = "23876213";
String postCode = "KT5 8LJ";
Address address = new Address(postCode, "London");
StudentEntity studentEntity = StudentEntity.builder()
.id(studentId)
.admitYear("2023")
.address(address)
.build();
StudentEntity savedStudentEntity = studentRepository.save(studentEntity);
Optional<StudentEntity> studentEntityOptional = studentRepository.findById(studentId);
assertThat(studentEntityOptional.isPresent()).isTrue();
studentEntity = studentEntityOptional.get();
assertThat(studentEntity.getId()).isEqualTo(studentId);
assertThat(studentEntity.getAddress().getPostCode()).isEqualTo(postCode);
}
當我們執行測試時,JPA 會觸發以下插入 SQL:
Hibernate:
insert
into
"public"
."student_str" ("address", "admit_year", "student_id")
values
(?, ?, ?)
binding parameter [1] as [VARCHAR] - [{"postCode":"KT6 7BB","city":"London"}]
binding parameter [2] as [VARCHAR] - [2023]
binding parameter [3] as [VARCHAR] - [23876371]
我們將看到第一個參數已被AddressAttributeConverter
從我們的Address
實例成功轉換並綁定為VARCHAR
類型。
3. JSONB
優於VARCHAR
我們已經探索了將 JSON 資料儲存在VARCHAR
列中的轉換。現在,讓我們將address
的欄位定義從VARCHAR
改為JSONB
:
CREATE TABLE student (
student_id VARCHAR(8) PRIMARY KEY,
admit_year VARCHAR(4),
address jsonb
);
當我們探索JSONB
資料類型時,經常會出現一個常見問題:使用JSONB
透過VARCHAR
在 PostgreSQL 中儲存 JSON 有什麼意義,因為它本質上是一個字串?
JSONB
是PostgreSQL中用來處理JSON資料的指定資料類型。該類型以分解的二進制格式儲存數據,由於額外的轉換,在儲存 JSON 時會產生一些開銷。
事實上,與VARCHAR
相比,它提供了額外的功能,使JSONB
成為在 PostgreSQL 中儲存 JSON 資料的更有利的選擇。
3.1.驗證
JSONB
類型對儲存的值強制進行資料驗證,以確保列值是有效的 JSON。 PostgreSQL 拒絕任何使用無效 JSON 值插入或更新資料的嘗試。
為了示範這一點,我們可以考慮一個插入 SQL 查詢,其中address
列的 JSON 值無效,其中ity
屬性末尾缺少雙引號:
INSERT INTO student(student_id, admit_year, address)
VALUES ('23134572', '2022', '{"postCode": "E4 8ST, "city":"London}');
在 PostgreSQL 中執行此查詢會導致驗證錯誤,指示 JSON 無效:
SQL Error: ERROR: invalid input syntax for type json
Detail: Token "city" is invalid.
Position: 83
Where: JSON data, line 1: {"postCode": "E4 8ST, "city...
3.2.查詢
PostgreSQL 支援在 SQL 查詢中使用 JSON 欄位進行查詢。 JPA 支援使用本機查詢來搜尋資料庫中的記錄。在 Spring Data 中,我們可以定義一個自訂查詢方法來尋找Student
清單:
@Repository
public interface StudentRepository extends CrudRepository<StudentEntity, String> {
@Query(value = "SELECT * FROM student WHERE address->>'postCode' = :postCode", nativeQuery = true)
List<StudentEntity> findByAddressPostCode(@Param("postCode") String postCode);
}
此查詢是本機 SQL 查詢,它選擇資料庫中address
JSON 屬性postCode
等於提供的參數的所有Student
實例。
3.3.索引
JSONB
支援 JSON 資料索引。當我們必須透過 JSON 列中的鍵或屬性查詢資料時,這為JSONB
提供了顯著的優勢。
JSON 欄位可以套用多種類型的索引,包括 GIN、HASH 和 BTREE。 GIN適合索引複雜的資料結構,包括陣列和JSON。當我們只需要考慮等號運算子=
時,HASH 就很重要。當我們處理<
和>=
等範圍運算子時,BTREE 允許有效率地查詢。
例如,如果我們總是需要根據address
列中的postCode
屬性檢索數據,我們可以建立以下索引:
CREATE INDEX idx_postcode ON student USING HASH((address->'postCode'));
JSONB
映射
當資料庫列定義為JSONB
時,我們無法套用相同的AttributeConverter
。如果我們嘗試執行以下操作,我們的應用程式將在啟動時拋出以下錯誤:
org.postgresql.util.PSQLException: ERROR: column "address" is of type jsonb but expression is of type character varying
即使我們更改AttributeConverter
類別定義以使用Object
作為轉換後的列值而不是String
情況也是如此:
@Converter
public class AddressAttributeConverter implements AttributeConverter<Address, Object> {
// 2 conversion methods implementation
}
我們的應用程式抱怨不支援的類型:
org.postgresql.util.PSQLException: Unsupported Types value: 1,943,105,171
這表示 JPA 本身不支援JSONB
類型。然而,我們的底層 JPA 實作 Hibernate 確實支援 JSON 自訂類型,允許我們將複雜類型對應到 Java 類別。
4.1. Maven依賴
實際上,我們必須為JSONB
轉換定義自訂類型。然而,由於現有的函式庫Hypersistence Utilities ,我們不必重新發明輪子。
Hypersistence Utilities 是 Hibernate 的通用實用程式庫。它的特點之一是具有針對不同資料庫(例如 PostgreSQL 和 Oracle)的 JSON 列類型映射的定義。因此,我們可以簡單地將這個額外的依賴項包含在pom.xml
中:
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-55</artifactId>
<version>3.7.0</version>
</dependency>
4.2.更新的實體類
Hypersistence Utilities 定義了依賴資料庫的不同自訂類型。在 PostgreSQL 中,我們將使用JsonBinaryType
類別作為JSONB
欄位類型。在我們的實體類別中,我們使用類別註解@TypeDef
定義自訂類型,然後透過@Type
將定義的類型套用到address
欄位:
@Entity
@Table(name = "student")
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class StudentEntity {
@Id
@Column(name = "student_id", length = 8)
private String id;
@Column(name = "admit_year", length = 4)
private String admitYear;
@Type(type = "jsonb")
@Column(name = "address", columnDefinition = "jsonb")
private Address address;
// getters and setters
}
對於使用@Type
的這種情況,我們不需要再將AttributeConverter
套用到address
欄位。 Hypersistence Utilities 中的自訂類型會為我們處理轉換任務,使我們的程式碼更加整潔。
4.3.測試用例
完成所有這些變更後,讓我們再次執行Student
持久性測試案例:
Hibernate:
insert
into
"public"
."student" ("address", "admit_year", "student_id")
values
(?, ?, ?)
binding parameter [1] as [OTHER] - [Address(postCode=KT6 7BB, city=London)]
binding parameter [2] as [VARCHAR] - [2023]
binding parameter [3] as [VARCHAR] - [23876371]
我們將看到 JPA 觸發與先前相同的插入 SQL,除了第一個參數綁定為OTHER
而不是VARCHAR
。這表示Hibernate這次將參數綁定為JSONB
類型。
5. 結論
這份全面的指南為我們提供了使用 Spring Boot 和 JPA 在 PostgreSQL 中熟練儲存和管理 JSON 資料的知識。
它解決了 JSON 值到VARCHAR
類型和JSONB
類型的對應。它還強調了JSONB
在執行 JSON 驗證以及促進查詢和索引方面的重要性。
與往常一樣,範例程式碼可在 GitHub 上取得。