解决 JPA 中的 PostgreSQL JSON 类型不匹配异常

1、简介

本文将带你了解在使用 JPA 与 PostgreSQL 交互时出现异常 “PSQLException error: column is of type json but the expression is of type character varying” 的原因,以及解决办法。

2、常见的原因

在 PostgreSQL 中,JSON 或 JSONB 数据类型用于存储 JSON 数据。但是,如果我们试图将字符串(character varyin)插入到期望使用 JSON 的列中,PostgreSQL 就会抛出 “column is of type json but expression is of type character varying” 错误。这种情况在使用 JPA 和 PostgreSQL 时尤其常见,因为 JPA 可能会尝试将字符串保存到 JSON 列,从而导致此错误。

3、异常演示

创建一个基本的 Spring Boot 项目,在 Maven pom.xml 文件中添加 PostgreSQL 依赖:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.1</version>
    <scope>runtime</scope>
</dependency>

创建一个映射到 student 表的 JPA 实体类:

@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String admitYear;

    @Column(columnDefinition = "json")
    private String address;

    // Getter / Setter 方法
}

如上, address 字段被映射到 student 表中的 address 列。而且,columnDefinition 属性被指定为 json,表明该列的类型是 JSON

现在,尝试将一个 Student 对象保存到数据库中:

Student student = new Student();
student.setAdmitYear("2024");
// json 紫都城
student.setAddress("{\"postCode\": \"TW9 2SF\", \"city\": \"London\"}");

Throwable throwable = assertThrows(Exception.class, () -> studentRepository.save(student));
assertTrue(ExceptionUtils.getRootCause(throwable) instanceof PSQLException);

如上,创建了一个 Student 对象,并将 address 字段设置为 JSON 字符串。然后,使用 studentRepository 对象的 save() 方法将此对象保存到数据库中。

然而,这会导致 PSQLException 异常:

Caused by: org.postgresql.util.PSQLException: ERROR: column "address" is of type json but expression is of type character varying

出现此错误的原因是 JPA 尝试将字符串保存到 JSON 列,而这是不允许的。

4、使用 @Type 注解

要解决这个错误,需要正确处理 JSON 类型。

可以使用 hibernate-types 库提供的 @Type 注解。

首先,在 pom.xml 中添加 hibernate-types 依赖:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>2.18.0</version>
</dependency>

然后,更新实体,添加 @TypeDef@Type 注解:

@Entity
@Table(name = "student_json")
@TypeDefs({
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class StudentWithTypeAnnotation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String admitYear;

    @Type(type = "jsonb")
    @Column(columnDefinition = "json")
    private String address;

    // Getter / Setter 方法
}

如上,@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) 注册了一个名为 JSONB 的自定义类型,该类型使用了 hibernate-types-52 库中的 JsonBinaryType 类。JsonBinaryType 处理 PostgreSQL 的 JSONB 数据类型,允许以 JSONB 的形式有效地存储和检索 JSON 数据。

@Type 注解用于为字段指定 Hibernate 的自定义类型。通过指定 @Type(type="jsonb"),我们告诉 Hibernate 使用通过 @TypeDef 注册的自定义类型 JSONB。该自定义类型可处理 PostgreSQL 中 Java 对象与 JSONB 数据之间的转换。

这种设置可确保在 PostgreSQL 中使用 JSONB 数据类型高效地存储和检索 JSON 数据:

StudentWithTypeAnnotation student = new StudentWithJson();
student.setAdmitYear("2024");
student.setAddress("{\"postCode\": \"TW9 2SF\", \"city\": \"London\"}");
studentWithTypeAnnotationRepository.save(student);

StudentWithTypeAnnotation retrievedStudent = studentWithTypeAnnotationRepository.findById(student.getId()).orElse(null);

assertThat(retrievedStudent).isNotNull();
assertThat(retrievedStudent.getAddress()).isEqualTo("{\"postCode\":\"TW9 2SF\",\"city\":\"London\"}");

5、原生查询

此外,当使用 @Query 注解原生 SQL 查询将 JSON 数据插入 PostgreSQL 表时,也会遇到同样的错误。

创建一个原生的 insert 查询来演示这个错误:

@Query(value = "INSERT INTO student (admit_year, address) VALUES (:admitYear, :address) RETURNING *", nativeQuery = true)
Student insertJsonData(@Param("admitYear") String admitYear, @Param("address") String address);

当使用 JSON 字符串调用该方法时,就会出现异常:

Throwable throwable = assertThrows(Exception.class, () -> 
  studentRepository.insertJsonData("2024","{\"postCode\": \"TW9 2SF\", \"city\": \"London\"}"));
assertTrue(ExceptionUtils.getRootCause(throwable) instanceof PSQLException);

要解决这个问题,需要在插入之前将 JSON 字符串转换为 JSONB 类型,以避免出现此错误:

public interface StudentWithTypeAnnotationRepository extends JpaRepository<StudentWithTypeAnnotation, Long> {
    @Query(value = "INSERT INTO student (admit_year, address) VALUES (:admitYear, CAST(:address AS JSONB)) RETURNING *", nativeQuery = true)
    StudentWithTypeAnnotation insertJsonData(@Param("admitYear") String admitYear, @Param("address") String address);
}

如上,使用 CAST(:address AS JSONB) 语法将 :address 参数转换为 JSONB 类型。

现在,测试一下这个方法:

StudentWithTypeAnnotation student = studentWithJsonRepository.insertJsonData("2024","{\"postCode\": \"TW9 2SF\", \"city\": \"London\"}");

StudentWithTypeAnnotation retrievedStudent = studentWithJsonRepository.findById(student.getId()).orElse(null);

assertThat(retrievedStudent).isNotNull();
assertThat(retrievedStudent.getAddress()).isEqualTo("{\"city\": \"London\", \"postCode\": \"TW9 2SF\"}");

6、总结

本文介绍了在使用 JPA 处理 PostgreSQL JSON 列时出现 PSQLException error “column is of type json but the expression is of type character varying” 异常的原因,以及解决办法。


Ref:https://www.baeldung.com/jpa-postgresql-json-type-mismatch-errors