1. 概述
本教程將提供對在 PostgreSQL 的 JSONB 列中存儲 JSON 數據的全面理解。
使用 JPA,我們將快速回顧如何處理存儲為可變字符 (VARCHAR) 的數據庫列中的 JSON 值。之後,我們將比較 VARCHAR 類型和 JSONB 類型的差異,以瞭解 JSONB 類型的額外功能。最後,我們將解決在 JPA 中映射 JSONB 類型的相關問題。
2. VARCHAR 映射
在這一部分,我們將探討如何將 JSON 值(類型為 VARCHAR)轉換為自定義 Java POJO。為此,我們將使用 AttributeConverter 以輕鬆地將 Java 數據類型中的實體屬性值轉換為數據庫列中的相應值。
2.1 Maven 依賴
要創建 屬性轉換器,我們需要在 pom.xml 中包含最新的 Spring Data JPA 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.3.2</version>
</dependency>2.2. 表定義
讓我們通過以下數據庫表定義來闡明這個概念:
CREATE TABLE student (
student_id VARCHAR(8) PRIMARY KEY,
admit_year VARCHAR(4),
address VARCHAR(500)
);學生表包含三個字段,我們期望地址列存儲包含以下結構的 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
}我們將對 address 字段進行標註,使用 @Convert 屬性,並應用 AddressAttributeConverter 將 Address 實例轉換為其 JSON 格式表示。
2.4. 屬性轉換器 (AttributeConverter)
之前,我們已經將實體類中的 address 字段映射為數據庫中的 VARCHAR 類型。但是,JPA 無法自動轉換自定義 Java 類型和 VARCHAR 類型。因此,屬性轉換器 填補了這一空白,通過提供機制來處理轉換過程。
我們使用 屬性轉換器 將自定義 Java 數據類型持久化到數據庫列中。 每一個 屬性轉換器 實現都需要定義兩個轉換方法。一個名為 convertToDatabaseColumn(),用於將 Java 數據類型轉換為其對應的數據庫數據類型,另一個名為 convertToEntityAttribute(),用於將數據庫數據類型轉換為 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;
}
}
}2.5. 測試用例
現在,我們可以驗證一個 學生 行是否正確持久化,以及其 地址:
@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 在插入新數據時執行的操作:
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]我們可以看到,第一個參數已成功地從我們的 Address 實例中轉換,並綁定為 VARCHAR 類型。
我們學習瞭如何通過將 JSON 數據轉換為 VARCHAR 以及反之來持久化 JSON 數據。接下來,讓我們檢查其他解決方案來處理 JSON 數據。
3. JSONB 優於 VARCHAR
在 PostgreSQL 中,我們可以將列的類型設置為 JSONB 以保存 JSON 數據:
CREATE TABLE student (
student_id VARCHAR(8) PRIMARY KEY,
admit_year VARCHAR(4),
address jsonb
);在這裏,我們定義了 address 列為 JSONB。這與之前使用的 VARCHAR 類型不同,並且重要的是要了解我們為什麼在 PostgreSQL 中使用這種數據類型。 JSONB 是 PostgreSQL 中處理 JSON 數據的一種指定數據類型。
此外,使用 JSONB 類型的列以分解二進制格式存儲數據,這在存儲 JSON 時存在一定的開銷,因為存在額外的轉換。
此外,JSONB 提供了與 VARCHAR 相比的額外功能。因此,JSONB 是在 PostgreSQL 中存儲 JSON 數據的一種更優選擇。
3.1. 驗證
JSONB 類型強制執行數據驗證,以確保列的值是一個有效的 JSON。 因此,嘗試向具有JSONB類型的列插入或更新包含無效 JSON 值的操作將失敗。
為了演示這一點,我們可以嘗試插入一個包含address列的無效 JSON 值,其中city屬性末尾缺少雙引號的 SQL 查詢:
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 支持使用 JSON 列在 SQL 查詢中進行查詢。 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 查詢,它從數據庫中選擇所有 Student 實例,其中 address JSON 屬性的 postCode 等於提供的參數。
3.3. 索引
JSONB 支持 JSON 數據索引。 這使得 JSONB 在我們必須通過 JSON 列中的鍵或屬性查詢數據時具有顯著優勢。
可以應用於 JSON 列的各種索引類型,包括 GIN、HASH 和 BTREE。 GIN 適用於索引複雜數據結構,包括數組和 JSON。 HASH 在我們僅需要考慮等於運算符 (=) 時非常重要。 BTREE 允許我們在處理範圍運算符(如 < 和 >=)時進行高效查詢。
例如,如果我們始終需要根據 postCode 屬性從 address 列中檢索數據,我們可以創建以下索引:
CREATE INDEX idx_postcode ON student USING HASH((address->'postCode'));4. JSONB 映射
我們不能在數據庫列定義為 JSONB 時應用相同的 屬性轉換器。否則,如果我們在嘗試:
org.postgresql.util.PSQLException: ERROR: column "address" is of type jsonb but the expression is of type character varying<p>即使我們更改 <em >AttributeConverter</em> 類定義,使用 <em >Object</em> 作為轉換的列值,而不是 <em >String</em>,我們仍然會得到一個錯誤:</p>
@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-60</artifactId>
<version>3.9.0</version>
</dependency>4.2. 更新後的實體類
Hypersistence Utilities 定義了不同的自定義類型,這些類型依賴於數據庫。在 PostgreSQL 中,我們將使用 <em class="em">JsonBinaryType</em> 類來處理 <em class="em">JSONB</em> 列類型。在我們的實體類中,我們使用 Hibernate 的 <em class="em">@TypeDef</em> 註解定義自定義類型,然後通過 <em class="em">@Type</em> 註解將該類型應用於 address 字段:
@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;
@Type(JsonBinaryType.class)
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "address", columnDefinition = "jsonb")
private Address address;
// getters and setters
}對於使用 的此用例,我們不再需要將 應用到 字段上。 Hypersistence Utilities 的自定義類型會自動處理轉換任務,使我們的代碼更簡潔。
注意: 註解在 Hibernate 6 中已棄用。
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 觸發與之前相同的 INSERT SQL 語句,只是第一個參數被綁定為 OTHER 而不是 VARCHAR。 這表明 Hibernate 現在將參數綁定為 JSONB 類型。
5. 結論
在本教程中,我們學習瞭如何使用 Spring Boot 和 JPA 在 PostgreSQL 中管理 JSON 數據。首先,我們學習瞭如何將 JSON 值映射到 VARCHAR 類型和 JSONB 類型,使用了自定義轉換器。然後,我們瞭解了使用 JSONB 強制 JSON 驗證以及輕鬆查詢和索引 JSON 值的重要性。最後,我們使用 Hypersistence 庫實現了 JSONB 列的自定義類型轉換。