繁体   English   中英

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

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

我在应用程序 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;

    }
}

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

为了将来参考,您可以使用CREATE DOMAIN在 H2 中定义自定义类型,如下所示:

CREATE domain IF NOT EXISTS jsonb AS other;

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

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

香槟时间! 🍾

从版本2.11开始, 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都可以工作。

如果您想查看它的实际效果,请查看GitHub 上的这个测试文件夹

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

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

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

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

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

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

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

来源文章

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

另一个问题是,当您插入一个 json 时,H2 会将其转换为一个json 对象字符串,这会导致 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; 
  }
}

而已。 我希望它可以帮助某人。

在我的例子中,我们在生产中使用PostgreSQL jsonb类型,在测试中使用H2

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

这是它的 要点

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

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

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

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 中定义每个数据库的自定义类型,如下所述: https ://stackoverflow.com/a/59753980/10714479

我和@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;

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

H2 没有 JSON 数据类型。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM