Spring Boot中用JPA存储PostgreSQL JSONB

本教程将使我们全面了解如何在 PostgreSQL JSONB列中存储 JSON 数据。

我们将快速回顾一下如何使用JPA处理存储在可变字符 ( VARCHAR ) 数据库列中的 JSON 值。之后,我们将比较VARCHAR类型和JSONB类型之间的差异,了解JSONB的附加功能。最后,我们将解决 JPA 中的映射JSONB类型。

VARCHAR映射_
在本节中,我们将探讨如何使用AttributeConverter将VARCHAR类型的 JSON 值转换为自定义 Java POJO 。

其目的是为了方便Java数据类型中的实体属性值和数据库列中对应的值之间的转换。

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.表定义
让我们使用以下数据库表定义通过一个简单的示例来说明这个概念:

CREATE TABLE student (
    student_id VARCHAR(8) PRIMARY KEY,
    admit_year VARCHAR(4),
    address VARCHAR(500)
);

学生表有三个字段,我们期望地址列存储具有以下结构的 JSON 值:

{
  "postCode": "TW9 2SF",
 
"city": "London"
}

3.实体类
为了处理这个问题,我们将创建一个相应的POJO类来表示 Java 中的地址数据:

public class Address {
    private String postCode;
    private String city;
    // constructor, getters and setters
}

接下来,我们将创建一个实体类StudentEntity,并将其映射到我们之前创建的学生表:

@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注释地址字段,并应用AddressAttributeConverter将Address实例转换为其 JSON 表示形式。

4.属性转换器
我们将实体类中的地址字段映射到数据库中的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()负责将数据库列值转换为相应的实体字段值。

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类型。

JSONB优于VARCHAR
我们已经探索了将 JSON 数据存储在VARCHAR列中的转换。现在,让我们将地址的列定义从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 数据的更有利的选择。

验证
JSONB类型对存储的值强制进行数据验证,以确保列值是有效的 JSON。 PostgreSQL 拒绝任何使用无效 JSON 值插入或更新数据的尝试。

为了演示这一点,我们可以考虑一个插入 SQL 查询,其中地址列的 JSON 值无效,其中城市属性末尾缺少双引号:

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...

查询
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 查询,它选择数据库中地址JSON 属性postCode等于提供的参数的所有Student实例。

索引
JSONB支持JSON数据索引。当我们必须通过 JSON 列中的键或属性查询数据时,这为JSONB提供了显着的优势。

JSON 列可以应用各种类型的索引,包括 GIN、HASH和BTREE。 GIN适合索引复杂的数据结构,包括数组和JSON。当我们只需要考虑等号运算符=时,HASH 就很重要。当我们处理<和>=等范围运算符时,BTREE 允许高效查询。

例如,如果我们总是需要根据地址列中的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 类。

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>

2.更新的实体类
Hypersistence Utilities 定义了依赖于数据库的不同自定义类型。在 PostgreSQL 中,我们将使用JsonBinaryType类作为JSONB列类型。在我们的实体类中,我们使用类注释@TypeDef定义自定义类型,然后通过@Type将定义的类型应用到地址字段:

@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应用于地址字段。 Hypersistence Utilities 中的自定义类型为我们处理转换任务,使我们的代码更加整洁。

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类型。

结论
这份全面的指南为我们提供了使用 Spring Boot 和 JPA 在 PostgreSQL 中熟练存储和管理 JSON 数据的知识。

它解决了 JSON 值到VARCHAR类型和JSONB类型的映射。它还强调了JSONB在执行 JSON 验证以及促进查询和索引方面的重要性。