如何使用SpringBoot + JPA存储PostgreSQL的jsonb数据类型?

41

我正在开发一款迁移软件,该软件将从REST服务中获取未知数据。

我最初考虑使用MongoDB,但决定不使用它,而是使用PostgreSQL。

在阅读了这篇文章后,我尝试在我的SpringBoot应用程序中使用Spring JPA实现它,但我不知道如何映射jsonb到我的实体。

我尝试了这篇文章,但什么也没懂!

这就是我目前的情况:

@Repository
@Transactional
public interface DnitRepository extends JpaRepository<Dnit, Long> {

    @Query(value = "insert into dnit(id,data) VALUES (:id,:data)", nativeQuery = true)
    void insertdata( @Param("id")Integer id,@Param("data") String data );

}

并且...

@RestController
public class TestController {

    @Autowired
    DnitRepository dnitRepository;  

    @RequestMapping(value = "/dnit", method = RequestMethod.GET)
    public String testBig() {
        dnitRepository.insertdata(2, someJsonDataAsString );
    }

}

以及表格:

CREATE TABLE public.dnit
(
    id integer NOT NULL,
    data jsonb,
    CONSTRAINT dnit_pkey PRIMARY KEY (id)
)

我该如何做到这一点?

注意:我不需要一个实体来工作。我的JSON将始终为字符串,但我需要使用jsonb查询数据库。


1
那么为什么还要使用JPA呢,你现在已经在编写本地查询了。 - M. Deinum
1
配置JDBC与Spring Data JPA有什么关系?你是从哪里得到这个想法的?Spring Data JPA与配置数据源和JdbcTemplate无关。即使不使用Spring Data JPA,两者都会自动配置。 - M. Deinum
1
你从哪里得到这个印象的?不,你不需要...Spring Boot仍然会配置它,Spring Data JPA并不是必需的!而且你也不需要JPA...你甚至都没有使用它,那为什么还要费心去处理它呢。 - M. Deinum
啊!现在我懂了!你能把它作为一个答案发布吗?(将我的控制器复制并带上你的解决方案会很好) - Magno C
仍有人在寻找解决方案,这可能会起作用 https://dev59.com/m1EG5IYBdhLWcg3weOu6#65489772 - Dinesh KD
显示剩余8条评论
5个回答

74

尝试了这个,但什么都没懂!

要在Spring Data JPA(Hibernate)项目中完全使用jsonb和Vlad Mihalcea的hibernate-types库,只需执行以下操作:

1)将此库添加到您的项目中:

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

2) 然后在您的实体中使用它的类型,例如:

@Data
@NoArgsConstructor
@Entity
@Table(name = "parents")
<b>@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)</b>
public class Parent implements Serializable {

    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Integer id;

    @Column(length = 32, nullable = false)
    private String name;

    <b>@Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")</b>
    private List<Child> children;

    <b>@Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")</b>
    private Bio bio;

    public Parent(String name, List children, Bio bio) {
        this.name = name;
        this.children = children;
        this.bio = bio;
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Child implements Serializable {
    private String name;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Bio implements Serializable {
    private String text;
}

那么您就可以使用例如简单的JpaRepository来处理您的对象:

public interface ParentRepo extends JpaRepository<Parent, Integer> {
}
parentRepo.save(new Parent(
                     "parent1", 
                     asList(new Child("child1"), new Child("child2")), 
                     new Bio("bio1")
                )
);
Parent result = parentRepo.findById(1);
List<Child> children = result.getChildren();
Bio bio = result.getBio();

那就是问题所在。正如我一开始所说:“我正在开发一个迁移软件,它将从REST服务中获取未知数据”,因此我无法创建任何实体。如果我有实体,那么JSON就没有必要了。M. Deinum的解决方案是最好的。 - Magno C
11
@MagnoC,我的回答并不是解决方案,只是对“hibernate-types”的帮助。 - Cepr0
3
如果这个世界上没有 Vlad,我们该怎么办? - René Winkler
@Cepr0 如果我只想将其与JsonObject映射,而不是任何自定义类呢? - Vishal_Kotecha
对于Hibernate 5.6,请按照此链接操作:https://vladmihalcea.com/how-to-map-json-objects-using-generic-hibernate-types/ - tutak

18
通过添加Spring Data JPA来执行一个简单的插入语句,你让事情变得过于复杂。你没有使用任何JPA功能。相反,可以按照以下步骤操作:
  1. spring-boot-starter-jdbc 替换 spring-boot-starter-data-jpa
  2. 删除 DnitRepository 接口
  3. 在注入 DnitRepository 的地方,改为注入 JdbcTemplate
  4. jdbcTemplate.executeUpdate("insert into dnit(id, data) VALUES (?,to_json(?))", id, data); 替换 dnitRepository.insertdata(2, someJsonDataAsString );
如果你需要使用纯SQL(并且不需要JPA),那么你已经在使用普通的SQL(以一种非常复杂的方式)。当然,你可能希望将 JdbcTemplate 直接注入到控制器中,而是将该逻辑/复杂性隐藏在仓库或服务中。

运行得非常好,非常感谢。该服务确实是必备的。等等...“复杂的方式”? - Magno C
4
“很复杂”指的是试图将其塞入JPA中,而不实际使用JPA并使用不必要的层。 - M. Deinum

13

已经有几个答案了,我相信它们适用于多种情况。我不想使用我不了解的任何其他依赖项,因此我寻找另一种解决方案。 重要的部分是AttributeConverter,它将来自数据库的jsonb映射到您的对象以及反之。因此,您必须使用@Convert注释JSONB列的属性,并将其链接到AttributeConverter,并添加@Column(columnDefinition = "jsonb"),这样JPA就知道在数据库中使用的类型。这应该已经可以启动Spring Boot应用程序了。但是,每当您尝试使用JpaRepository进行save()时,都会遇到问题。我收到了以下消息:

PSQLException: ERROR: column "myColumn" is of type jsonb but expression is of type character varying.

Hint: You will need to rewrite or cast the expression.

这是因为Postgres太过严格地处理类型。 您可以通过更改配置来解决此问题:

datasource.hikari.data-source-properties: stringtype=unspecified

datasource.tomcat.connection-properties: stringtype=unspecified

之后,它对我非常有效,这里是一个最简示例。 我使用JpaRepositories:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Integer> {
}

实体:

import javax.persistence.Column;
import javax.persistence.Convert;

public class MyEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  protected Integer id;

  @Convert(converter = MyConverter.class)
  @Column(columnDefinition = "jsonb")
  private MyJsonObject jsonContent;

}

json的数据模型:

public class MyJsonObject {

  protected String name;

  protected int age;

}

转换器,我在这里使用的是Gson,但你可以按照自己的喜好进行映射:

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class MyConverter implements AttributeConverter<MyJsonObject, String> {

  private final static Gson GSON = new Gson();

  @Override
  public String convertToDatabaseColumn(MyJsonObject mjo) {
    return GSON.toJson(mjo);
  }

  @Override
  public MyJsonObject convertToEntityAttribute(String dbData) {
    return GSON.fromJson(dbData, MyJsonObject.class);
  }
}

SQL:

create table my_entity
(
    id serial primary key,
    json_content jsonb

);

我的application.yml(application.properties)文件:

  datasource:
    hikari:
      data-source-properties: stringtype=unspecified
    tomcat:
      connection-properties: stringtype=unspecified

1
很棒的答案!省了我大量的时间。这里描述的一件事对我没有起作用,那就是将 stringtype 设置为 unspecified;我唯一能够做到的方式是在连接 URL 中明确提供,例如:jdbc:postgresql://localhost:5432/mydb?stringtype=unspecified - Arthur
谢谢Arthur,将这个参数添加到我的连接字符串中解决了问题。 - Tom Drake
如果JSON类型的对象类(MyJsonObject)是MyJsonObject类的列表,该怎么办? - Anjani Mittal

4
对于这种情况,我使用上述定制的转换器类,您可以将其添加到您的库中。它可以与EclipseLink JPA提供程序一起使用。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.log4j.Logger;
import org.postgresql.util.PGobject;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;

@Converter
public final class PgJsonbToMapConverter implements AttributeConverter<Map<String, ? extends Object>, PGobject> {

    private static final Logger LOGGER = Logger.getLogger(PgJsonbToMapConverter.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public PGobject convertToDatabaseColumn(Map<String, ? extends Object> map) {
        PGobject po = new PGobject();
        po.setType("jsonb");

        try {
            po.setValue(map == null ? null : MAPPER.writeValueAsString(map));
        } catch (SQLException | JsonProcessingException ex) {
            LOGGER.error("Cannot convert JsonObject to PGobject.");
            throw new IllegalStateException(ex);
        }
        return po;
    }

    @Override
    public Map<String, ? extends Object> convertToEntityAttribute(PGobject dbData) {
        if (dbData == null || dbData.getValue() == null) {
            return null;
        }
        try {
            return MAPPER.readValue(dbData.getValue(), new TypeReference<Map<String, Object>>() {
            });
        } catch (IOException ex) {
            LOGGER.error("Cannot convert JsonObject to PGobject.");
            return null;
        }
    }

}

以下是一个名为 Customer 的实体的使用示例。

@Entity
@Table(schema = "web", name = "customer")
public class Customer implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Convert(converter = PgJsonbToMapConverter.class)
    private Map<String, String> info;

    public Customer() {
        this.id = null;
        this.info = null;
    }

    // Getters and setter omitted.

有人能告诉我如何检索与Jsonb列中的对象匹配的数据吗? - HARISH
这是一个不同的问题,您可以找到类似的问题或添加自己的问题以获得答案。您可能需要正确配置查询的“where”部分。 - Georgios Syngouroglou
使用Hibernate时,我收到了错误消息“列“info”的类型为jsonb,但表达式的类型为bytea”。有人能够在Hibernate中使用这个解决方案吗? - fap

1
如果您正在使用R2DBC,可以使用依赖项io.r2dbc:r2dbc-postgresql,并在实体类的成员属性中使用类型io.r2dbc.postgresql.codec.Json,例如:
public class Rule {
    @Id
    private String client_id;
    private String username;
    private String password;
    private Json publish_acl;
    private Json subscribe_acl;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接