如何使用 H2、JPA 和 Hibernate 映射 JSON 列

How to map a JSON column with H2, JPA, and Hibernate

我在应用程序 MySQL 5.7 中使用并且我有 JSON 列。当我尝试 运行 时,我的集成测试不起作用,因为 H2 数据库无法创建 table。这是错误:

2016-09-21 16:35:29.729 ERROR 10981 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000389: Unsuccessful: create table payment_transaction (id bigint generated by default as identity, creation_date timestamp not null, payload json, period integer, public_id varchar(255) not null, state varchar(255) not null, subscription_id_zuora varchar(255), type varchar(255) not null, user_id bigint not null, primary key (id))
2016-09-21 16:35:29.730 ERROR 10981 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : Unknown data type: "JSON"; SQL statement:

这是实体 class。

@Table(name = "payment_transaction")
public class PaymentTransaction extends DomainObject implements Serializable {

    @Convert(converter = JpaPayloadConverter.class)
    @Column(name = "payload", insertable = true, updatable = true, nullable = true, columnDefinition = "json")
    private Payload payload;

    public Payload getPayload() {
        return payload;
    }

    public void setPayload(Payload payload) {
        this.payload = payload;
    }
}

和子class:

public class Payload implements Serializable {

    private Long userId;
    private SubscriptionType type;
    private String paymentId;
    private List<String> ratePlanId;
    private Integer period;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public SubscriptionType getType() {
        return type;
    }

    public void setType(SubscriptionType type) {
        this.type = type;
    }

    public String getPaymentId() {
        return paymentId;
    }

    public void setPaymentId(String paymentId) {
        this.paymentId = paymentId;
    }

    public List<String> getRatePlanId() {
        return ratePlanId;
    }

    public void setRatePlanId(List<String> ratePlanId) {
        this.ratePlanId = ratePlanId;
    }

    public Integer getPeriod() {
        return period;
    }

    public void setPeriod(Integer period) {
        this.period = period;
    }

}

这个用于插入数据库的转换器:

public class JpaPayloadConverter implements AttributeConverter<Payload, String> {

    // ObjectMapper is thread safe
    private final static ObjectMapper objectMapper = new ObjectMapper();

    private Logger log = LoggerFactory.getLogger(getClass());

    @Override
    public String convertToDatabaseColumn(Payload attribute) {
        String jsonString = "";
        try {
            log.debug("Start convertToDatabaseColumn");

            // convert list of POJO to json
            jsonString = objectMapper.writeValueAsString(attribute);
            log.debug("convertToDatabaseColumn" + jsonString);

        } catch (JsonProcessingException ex) {
            log.error(ex.getMessage());
        }
        return jsonString;
    }

    @Override
    public Payload convertToEntityAttribute(String dbData) {

        Payload payload = new Payload();
        try {
            log.debug("Start convertToEntityAttribute");

            // convert json to list of POJO
            payload = objectMapper.readValue(dbData, Payload.class);
            log.debug("JsonDocumentsConverter.convertToDatabaseColumn" + payload);

        } catch (IOException ex) {
            log.error(ex.getMessage());
        }
        return payload;

    }
}

JSON在提问后对H2添加了支持,版本为1.4.200(2019-10-14)。

但是,您很少需要数据库中的 JSON 数据类型。 JSON 本质上只是一个可能很长的字符串,因此您可以使用大多数数据库都可用的 CLOB。

如果您需要对它们进行操作的 SQL 函数,那么您确实需要 JSON 数据类型,然后仅当数据库坚持其 JSON 函数对 JSON 类型而不是在 CLOB 上。不过,此类功能往往依赖于数据库。

我已经在H2 中使用TEXT 类型解决了这个问题。 必须创建一个单独的数据库脚本来在 H2 中创建用于测试的模式,并将 JSON 类型替换为 TEXT。

这仍然是一个问题,因为如果您在查询中使用 Json 函数,您将无法在使用 H2 时测试这些函数。

我刚刚在使用 JSONB 列类型时遇到了这个问题 - JSON 类型的二进制版本,它不映射到 TEXT.

为了以后参考,可以在H2中使用CREATE DOMAIN定义一个自定义类型,如下:

CREATE domain IF NOT EXISTS jsonb AS other;

这似乎对我有用,并允许我针对实体成功测试我的代码。

来源:https://objectpartners.com/2015/05/26/grails-postgresql-9-4-and-jsonb/

H2 没有 JSON 数据类型。

在 MySQL 中,JSON 类型只是 LONGTEXT 数据类型的别名,因此该列的实际数据类型将是 LONGTEXT。

在我的例子中,我们在生产中处理 PostgreSQL jsonb 类型,在我们的测试中处理 H2 类型。

我无法测试@n00dle 的解决方案,因为显然 spring 不支持在 Hibernateddl-auto=update 之前为我们的测试执行 SQL 脚本,所以我使用另一种方法来解决这个问题。

这是一个gist

总体思路是创建两个 package-info 文件。 一个用于生产,另一个用于测试,并注册不同的类型(JsonBinaryType.class 用于生产,TextType.class 用于测试)以对 PostgreSQLH2

进行不同的处理

解决方法实际上是在 H2 中为 jsonb 类型创建自定义列数据类型,并将查询放在数据源 url 中,如下所示:

spring.datasource.url=jdbc:h2:mem:testdb;INIT=create domain if not exists jsonb as text;MODE=PostgreSQL"

现在,特别是对于测试和集成测试,最好使用与您的应用程序相同的数据库,通过 TestContainers

我的问题出在 JSONB 上,因为正如已经提到的那样,H2 不支持它。

还有一个问题是,当你插入一个 json 时,H2 将其转换为 ,这使得 jackson 序列化失败。例如: "{\"key\": 3}" 而不是 {"key": 3} 。一种解决方案是在插入 json 时使用 FORMAT JSON,但是如果您使用 flyway,则需要重复插入文件。

受到 答案的启发,我想到了这个解决方案:

创建自定义 JsonbType(在生产中 - 例如 main/java/com/app/types/JsonbType.java)

import com.vladmihalcea.hibernate.type.json.JsonBinaryType;

public class JsonbType extends JsonBinaryType {
  private static final long serialVersionUID = 1L;
}

创建自定义 JsonbType(在测试中 - 例如 test/java/com/app/types/JsonbType.java)

import com.vladmihalcea.hibernate.type.json.JsonStringType;

public class JsonbType extends JsonStringType {
  private static final long serialVersionUID = 1L;
  @Override
  public String getName() {
      return "jsonb";
  }
}

仅在测试时创建从 JSONB 到 JSON 的别名类型 (h2):

-- only on H2 database
CREATE TYPE "JSONB" AS TEXT;

注意:我使用的是 flyway,这很容易做到,但您可以遵循 建议

最后在实体模型上声明类型,如下:

import com.app.types.JsonbType;

@TypeDef(name = "jsonb", typeClass = JsonbType.class)
@Entity(name = "Translation")
@Table(name = "Translation")
@Data
public class Translation {
  @Type(type = "jsonb")
  @Column(name="translations")
     private MySerializableCustomType translations; 
  }
}

就是这样。我希望它对某人有所帮助。

我和 的情况一样,我们在生产中使用 Postgres,在单元测试中使用 H2。就我而言,我认为我找到了一个更简单的解决方案。 我们使用 Liquibase 进行数据库迁移,所以这里我只在 H2 上进行了条件迁移 运行,其中我将列类型更改为 H2 的 "other" 类型。

对于另一种类型,H2 只是将其存储在数据库中,不会再考虑数据的格式等问题。但这确实需要您不直接对 JSON 进行任何操作数据库,并且仅在您的应用程序中。

我的迁移是这样的:

  # Use other type in H2, as jsonb is not supported
  - changeSet:
      id: 42
      author: Elias Jørgensen
      dbms: h2
      changes:
        - modifyDataType:
            tableName: myTableName
            columnName: config
            newDataType: other

与此同时,我将以下内容添加到我的测试数据源中:

INIT=create domain if not exists jsonb as text;

这就是我在 Spring 上下文中解决它的方法:

  1. 创建/src/test/resources/init.sql
CREATE TYPE "JSONB" AS json;
  1. 配置H2数据源如下/src/test/resources/application-test.yml
spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:init.sql'
    username: sa
    password: sa

Source article

Kotlin 示例 + Spring + Hibernate + Postgres + jsonb 列

创建实体:

@Entity
@TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
class MyEntity(
    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    val myConfig: String,

    @Id
    @GeneratedValue
    val id: Long = 0,
)

JsonBinaryType.class 来自 https://github.com/vladmihalcea/hibernate-types

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

在 spring 配置文件中配置 H2 数据库。关键行是这样的:INIT=create domain if not exists jsonb as other

spring:
    profiles: h2

    datasource:
        driver-class-name: org.h2.Driver
        url: jdbc:h2:mem:testdb;INIT=create domain if not exists jsonb as other;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
        username: sa
        password: sa

spring.jpa.hibernate.ddl-auto: create

编写测试:

// Postgres test
@SpringBootTest
class ExampleJsonbPostgres(@Autowired private val myEntityRepository: MyEntityRepository) {
    @Test
    fun `verify we can write and read jsonb`() {
        val r = myEntityRepository.save(MyEntity("""{"hello": "world"}"""))
        assertThat(myEntityRepository.findById(r.id).get().config).isEqualTo("""{"hello": "world"}""")
    }
}

// H2 test
@ActiveProfiles("h2")
@SpringBootTest
class ExampleJsonbH2(@Autowired private val myEntityRepository: MyEntityRepository) {
    @Test
    fun `verify we can write and read jsonb`() {
        val r = myEntityRepository.save(MyEntity("""{"hello": "world"}"""))
        assertThat(myEntityRepository.findById(r.id).get().config).isEqualTo("""{"hello": "world"}""")
    }
}

或者,您可以尝试在休眠 XML 中为每个数据库定义自定义类型,如下所述:

香槟时间!

从版本 2.11 开始,Hibernate Types project 现在提供了一个通用的 JsonType,它可以自动神奇地与以下对象一起工作:

  • 甲骨文,
  • SQL 服务器,
  • PostgreSQL,
  • MySQL,以及
  • H2.

甲骨文

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(name = "json", typeClass = JsonType.class)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    @Column(length = 15)
    private String isbn;

    @Type(type = "json")
    @Column(columnDefinition = "VARCHAR2(1000) CONSTRAINT IS_VALID_JSON CHECK (properties IS JSON)")
    private Map<String, String> properties = new HashMap<>();
}

SQL 服务器

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(name = "json", typeClass = JsonType.class)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    @Column(length = 15)
    private String isbn;

    @Type(type = "json")
    @Column(columnDefinition = "NVARCHAR(1000) CHECK(ISJSON(properties) = 1)")
    private Map<String, String> properties = new HashMap<>();
}

PostgreSQL

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(name = "json", typeClass = JsonType.class)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    @Column(length = 15)
    private String isbn;

    @Type(type = "json")
    @Column(columnDefinition = "jsonb")
    private Map<String, String> properties = new HashMap<>();
}

MySQL

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(name = "json", typeClass = JsonType.class)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    @Column(length = 15)
    private String isbn;

    @Type(type = "json")
    @Column(columnDefinition = "json")
    private Map<String, String> properties = new HashMap<>();
}

H2

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(name = "json", typeClass = JsonType.class)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    @Column(length = 15)
    private String isbn;

    @Type(type = "json")
    @Column(columnDefinition = "json")
    private Map<String, String> properties = new HashMap<>();
}

很有魅力!

因此,无需更多的技巧和解决方法,无论您使用什么数据库,JsonType 都可以正常工作。

如果您想实际查看它,请查看 this test folder on GitHub

避免此类事情的正确方法是使用 liquibaseflywaydb 来改进您的模式并且永远不要让 Hibernate 创建它。