【问题标题】:How to map a JSON column with H2, JPA, and Hibernate如何使用 H2、JPA 和 Hibernate 映射 JSON 列
【发布时间】:2017-01-29 22:13:45
【问题描述】:

我在应用程序 MySQL 5.7 中使用并且我有 JSON 列。当我尝试运行集成测试时,由于 H2 数据库无法创建表,因此无法正常工作。这是错误:

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:

这是实体类。

@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;
    }
}

还有子类:

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;

    }
}

【问题讨论】:

  • 解决了这个问题吗?我也在尝试做类似的事情但没有成功

标签: java spring hibernate h2 mysql-5.7


【解决方案1】:

我刚刚在使用 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/

【讨论】:

  • 嗨,我可以问你更多关于这个吗?我相信我有完全相同的问题,但是当我将该行放在创建表格的位置之前时,我收到错误type not found or user lacks privilege: NOT
  • 恐怕我不再从事这种事情了。不过我之前没有看到过这个错误。
  • 这是 H2 版本 的一个很好的解决方法
【解决方案2】:

香槟时间! ?

2.11.0 版本开始,Hibernate Types 项目现在提供了一个通用的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

【讨论】:

  • 如果我们只在测试上下文中使用 H2,是否仍然可以使用这个解决方案?我在生产中使用 Postgres (columnDefinition = "jsonb),在一些测试用例中使用 H2 (columnDefinition = "json")。
  • 准备好让自己大吃一惊。试试看吧。
【解决方案3】:

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

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

如果您需要一个对 JSON 数据类型进行操作的 SQL 函数,那么您确实需要 JSON 数据类型,并且只有当数据库坚持其 JSON 函数对 JSON 类型而不是 CLOB 进行操作时才需要。不过,此类函数往往依赖于数据库。

【讨论】:

  • 更新:从 1.4.200 开始,H2 确实具有原生 JSON 类型:h2database.com/html/changelog.html
  • @Type(type = "jsonb") @Column(columnDefinition = "jsonb") 这不受 h2 数据库支持
  • @Minisha 支持什么? (以防万一)
【解决方案4】:

一个变通的方法其实就是在H2中为jsonb类型创建一个自定义的列数据类型,将查询放到datasource url中如下:

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

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

【讨论】:

  • 支持 TestContainers 参考。如果这不是我们在生产中使用的,我们应该停止使用内存数据库进行集成测试。
  • MODE=PostgreSQL 是做什么的?
【解决方案5】:

这就是我在 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

【讨论】:

  • 这对我有用!我特别喜欢不需要对src/main/java 文件夹进行任何更改的方法。就我而言,我想使用@DataJpaTest,所以我还需要添加@AutoConfigureTestDatabase(replace = Replace.NONE) 以从src/test/resources/application.properties 中获取覆盖。
【解决方案6】:

在我的例子中,我们在生产中处理PostgreSQLjsonb,在我们的测试中使用H2

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

这是一个gist

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

【讨论】:

  • 提到的要点建议删除@Column(columnDefinition = "jsonb"),但为什么不将columnDefinition改为text
  • 我不知道,因为我不清楚结果。但即使它有效,我认为模型字段上的文本列类型不会增加任何好处!它只是在制造一些误导性信息!
  • 但如果没有 columnDefinition="jsonb" 如果它不存在,则无法在表中创建列。你是怎么解决的?
  • 对于数据库,我们使用的是 db 迁移,所以这不是我们关心的问题
【解决方案7】:

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

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

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

创建自定义 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";
  }
}

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

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

注意:我使用的是flyway,这很容易做到,但你可以关注@jchrbrt的建议

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

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; 
  }
}

就是这样。我希望它可以帮助某人。

【讨论】:

  • 我非常喜欢这个解决方案,很高兴您在两个小时前提出了这个解决方案。根据该线程上的活动,似乎每个人都在 2020 年 3 月在 Postgres 中编写有关 jsonb 类型的单元测试。
【解决方案8】:

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

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

【讨论】:

  • 不幸的是,我将不得不使用其他东西,因为我正在使用 Spring @Type 注释,如果我将 JSON 类型更改为 TEXT 或 CLOB 到 liquibase 更改日志文件,则会失败。
  • 如果您使用不同的数据库类型(如 H2),我建议您为测试停用休眠表创建。使用 spring 配置文件停用休眠自动创建并使用 liquibase 配置文件为您的测试环境使用不同的脚本。
  • 您还可以使用具有不同配置文件的 Spring 配置来管理测试实体与产品实体,并且仍然使用 @Type 和休眠自动创建。然后,您必须在不同的包中创建“测试”实体。在我看来,这是一个糟糕的设计,可能有点矫枉过正,我会亲自停用自动创建并使用具有不同 liquibase 配置文件的自定义 liquibase 脚本。在我自己的观点和经验中,Hibernate 表管理过于僵化,无法与多个数据库一起使用。
  • 感谢您的回复 :) 我设法用 EmbeddedPostgreSQL 替换 H2 来运行测试。启动速度有点慢,但它会阻止我自己未来的问题。 Liquibase 不是 100% DB 不可知论者,他们做得很好,但我在尝试使用 Oracle/Postgres 开关和 Postgres/H2 时遇到了一些问题。
【解决方案9】:

我的情况与@madz 相同,我们在生产中使用 Postgres,在单元测试中使用 H2。就我而言,我认为我找到了一个更简单的解决方案。 我们使用 Liquibase 进行数据库迁移,所以这里我做了一个仅在 H2 上运行的条件迁移,我将列类型更改为 H2 的“其他”类型。

对于另一种类型,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;

【讨论】:

    【解决方案10】:

    以 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"}""")
        }
    }
    

    或者,您可以尝试在 hibernate XML 中为每个数据库定义自定义类型,如下所述:https://stackoverflow.com/a/59753980/10714479

    【讨论】:

      【解决方案11】:

      避免此类事情的正确方法是使用 liquibaseflywaydb 来发展您的架构,并且永远不要让 Hibernate 创建它。

      【讨论】:

        【解决方案12】:

        H2 没有 JSON 数据类型。

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

        【讨论】:

          猜你喜欢
          • 2022-01-13
          • 2017-11-02
          • 2023-03-20
          • 2023-04-03
          • 1970-01-01
          • 1970-01-01
          • 2014-11-02
          • 2011-08-31
          • 1970-01-01
          相关资源
          最近更新 更多