51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用 Spring Data JPA 在 PostgreSQL 中存储和检索 JSON 数据,

1、概览 {#1概览}

本文将带你全面了解如何使用 Spring Data JPA 在 PostgreSQL JSONB 列中存储、检索 JSON 数据。

2、VARCHAR 映射 {#2varchar-映射}

本节将介绍如何使用 AttributeConverterVARCHAR 类型的 JSON 值转换为自定义 Java POJO。

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

2.1、Maven 依赖 {#21maven-依赖}

要创建 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、数据表定义 {#22数据表定义}

数据库表定义如下:

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、Entity 类 {#23entity-类}

创建一个相应的 POJO 类,用 Java 表示 address 数据:

public class Address {
    private String postCode;

    private String city;

    // 构造函数、Getter、Setter 省略
}

接下来,创建一个实体类 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;

    // 构造函数、Getter、Setter 省略
}

使用 @Convertaddress 字段进行注解,并指定 AddressAttributeConverterAddress 实例转换为 JSON 表示形式。

2.4、AttributeConverter {#24attributeconverter}

我们将实体类中的 address 字段映射为数据库中的 VARCHAR 类型。但是,JPA 无法自动执行自定义 Java 类型和 VARCHAR 类型之间的转换。

AttributeConverter 提供了一种处理转换过程的机制,从而弥补了这一缺陷。每个 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、测试 {#25测试}

创建一个测试用例,在数据库中持久化一个 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 会执行以下 insert 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]

如你所见,第一个参数已被 AddressAttributeConverterAddress 实例中成功转换,并绑定为 VARCHAR 类型。

3、JSONB 比 VARCHAR 更适合 {#3jsonb-比-varchar-更适合}

现在,将 address 的列定义从 VARCHAR 改为 JSONB

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

你可能会有一个问题:既然 JSONB 本质上是字符串,那么在 PostgreSQL 中使用 JSONB 存储 JSON 比使用 VARCHAR 有什么好处?

JSONB 是一种指定的数据类型,用于在 PostgreSQL 中处理 JSON 数据。该类型以分解的二进制格式存储数据,由于需要进行额外的转换,因此在存储 JSON 时会产生一些开销。

事实上,与 VARCHAR 相比,JSONB 提供了更多的功能,使其成为在 PostgreSQL 中存储 JSON 数据的更有利选择。

3.1、验证 {#31验证}

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

测试插入一个无效的 JSON 值,其中 address 列的 city 属性末尾缺少双引号:

INSERT INTO student(student_id, admit_year, address) 
VALUES ('23134572', '2022', '{"postCode": "E4 8ST, "city":"London}');

在 PostgreSQL 中执行此 SQL 会出现验证错误,表明 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、查询 {#32查询}

PostgreSQL 支持在 SQL 查询中使用 JSON 列进行查询。JPA 支持使用本地查询(nativeQuery)来检索数据库中的记录。在 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、索引 {#33索引}

JSONB 支持 JSON 数据索引。这使 JSONB 在根据 JSON 列中的 key 或属性查询数据时具有显著优势。

可以对 JSON 列应用各种类型的索引,包括 GINHASHBTREEGIN 适用于复杂数据结构的索引,包括数组和 JSON。当处理 <>= 等范围运算符时,BTREE 可以实现高效查询。

例如,如果我们经常需要根据 address 中的 postCode 属性检索数据,那么可以创建以下索引:

CREATE INDEX idx_postcode ON student USING HASH((address->'postCode'));

4、JSONB 映射 {#4jsonb-映射}

当数据库列定义为 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> {
    // 转换方法的实现
}

应用依然启动异常,表示不支持该类型:

org.postgresql.util.PSQLException: Unsupported Types value: 1,943,105,171

这表明 JPA 本身不支持 JSONB 类型。不过,底层 JPA 实现 Hibernate 确实支持 JSON 自定义类型,允许将复杂类型映射到 Java 类。

4.1、Maven 依赖 {#41maven-依赖}

我们需要为 JSONB 转换定义一个自定义类型。不过,不必重新发明轮子,因为已有一个 Hypersistence Utils 库。

Hypersistence UtilsHibernate 的通用工具库。它的功能之一是为 PostgreSQL 和 Oracle 等不同数据库定义 JSON 列类型映射。因此,我们只需在 pom.xml 中加入这个额外的依赖项即可:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>3.7.0</version>
</dependency>

4.2、更新实体类 {#42更新实体类}

Hypersistence Utils 定义了不同的自定义类型,这些类型取决于数据库。在 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;

    // Get、Set 省略
}

在使用 @Type 的情况下,不再需要对 address 字段应用 AttributeConverter。来自 Hypersistence Utils 的自定义类型会为我们处理转换任务,使我们的代码更加简洁。

4.3. 测试 {#43-测试}

完成所有这些更改后,再次运行 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、总结 {#5总结}

本文介绍了如何使用 Spring Data Jpa 在 PostgreSQL 中存储和检索 JSON 数据,还介绍了 VARCHARJSONB 类型之间的区别。


Ref:https://www.baeldung.com/spring-boot-jpa-storing-postgresql-jsonb

赞(1)
未经允许不得转载:工具盒子 » 使用 Spring Data JPA 在 PostgreSQL 中存储和检索 JSON 数据,