55

I use in application MySQL 5.7 and I have JSON columns. When I try running my integration tests don't work because the H2 database can't create the table. This is the error:

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:

This is the entity 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;
    }
}

And the subclass:

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

}

And this converter for insert in database:

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;

    }
}
Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
earandes
  • 553
  • 1
  • 4
  • 5

12 Answers12

50

I just came across this problem working with the JSONB column type - the binary version of the JSON type, which doesn't map to TEXT.

For future reference, you can define a custom type in H2 using CREATE DOMAIN, as follows:

CREATE domain IF NOT EXISTS jsonb AS other;

This seemed to work for me, and allowed me to successfully test my code against the entity.

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

n00dle
  • 5,949
  • 2
  • 35
  • 48
  • Hi, could I ask you more about this? I have the exact same issue I believe, but when I put that line before the place where I create the table, I get an error `type not found or user lacks privilege: NOT` – Matt Takao Mar 12 '19 at 21:27
  • I'm afraid I'm no longer working on this sort of thing. I've not seen that error before though. – n00dle Mar 13 '19 at 11:04
  • This is a nice workaround for H2 version < 1.4.200 – MaxXFrenzY May 03 '21 at 12:59
  • @MattTakao - possibly your database doesn't support the `if not exists` syntax, so try simply `create domain jsonb as other;` – Rick Gagne Nov 15 '22 at 01:03
48

Champagne time!

Starting with the version 2.11, the Hypersistence Utils project now provides a generic JsonType that works auto-magically with:

  • Oracle,
  • SQL Server,
  • PostgreSQL,
  • MySQL, and
  • H2.

Oracle

@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 Server

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

Works like a charm!

So, no more hacks and workarounds, the JsonType will work no matter what DB you are using.

If you want to see it in action, check out this test folder on GitHub.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
20

A workaround is actually to create a custom column data type in H2 for the jsonb type, and put the query in the datasource url like this:

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

Now for tests and integration tests in particular, it would be preferable to use the same DB than your application, via TestContainers

jchrbrt
  • 1,006
  • 1
  • 11
  • 12
  • 2
    Upvoting for the TestContainers reference. We should stop doing integration tests with in memory databases if that isn't what we use in production. – Jens Schauder Mar 05 '21 at 09:13
  • 2
    What does `MODE=PostgreSQL` do? – Milanka Mar 09 '21 at 16:01
  • Use TestContainers. I spent some time battling this same issue with SpringBoot 3 and hibernate 6, integration tests were failing but the application was able to start up, cause it uses PostgreSQL. I gave up, removed H2, and replaced it with TestContainers. Every thing is fine again, and it took really little time and effort to integrate. – Eteka Christopher Jan 06 '23 at 23:44
14

JSON support was added to H2 after the question was asked, with version 1.4.200 (2019-10-14).

However, you rarely need a JSON data type in a database. JSON essentially is just a potentially very long string, so you can use CLOB which is available on most databases.

You do need the JSON data type if you need an SQL function that operates on them, and then only if the database insists that its JSON functions operate on a JSON type instead of on a CLOB. Such functions tend to be database-dependent though.

toolforger
  • 754
  • 7
  • 22
11

My problem was with JSONB since H2 does not support it as was already mentioned.

One more problem is that when you insert a json, H2 transforms it into a json object string which makes jackson serialization fail. ex: "{\"key\": 3}" instead of {"key": 3} . One solution is to use FORMAT JSON when inserting the json, but then you need to have duplicate insert files if you are using flyway, for example.

Inspired by the @madz answer I came across with this solution:

Create a custom JsonbType (on production - e.g. 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;
}

Create a custom JsonbType (on tests - e.g. 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";
  }
}

Create an alias type from JSONB to JSON only on tests (h2):

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

note: I'm using flyway which make it easy to do but you can follow @jchrbrt suggestion

Finally you declare the type on your entity model, as follows:

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

That's it. I hope it helps someone.

Eduardo Pinheiro
  • 3,409
  • 3
  • 30
  • 39
  • 3
    I like this solution quite a bit, and I'm glad you came up with this two hours ago. It seems everyone is writing unit tests regarding jsonb types in Postgres in March 2020, based on the activity on this thread. – Mike Melusky Mar 23 '20 at 23:30
  • 1
    I used this solution, and I was able to save json to H2 but reading them fails and hibernate throws `Cannot construct instance of (although at least one Creator exists)`. It was solved by using `com.vladmihalcea.hibernate.type.json.JsonType` instead of `JsonStringType` so that serialization AND deserialization works – Kyle Domingo Feb 27 '23 at 17:07
11

This is how I solved it in Spring context:

  1. Create /src/test/resources/init.sql
CREATE TYPE "JSONB" AS json;
  1. Configure H2 datasource as follows /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

zjor
  • 994
  • 2
  • 12
  • 22
  • This works for me! I particularly like the approach for not requiring any changes in the `src/main/java` folder. In my case, I wanted to use `@DataJpaTest` so I needed to also add `@AutoConfigureTestDatabase(replace = Replace.NONE)` to pick up overrides from `src/test/resources/application.properties`. – devrogs Feb 03 '21 at 10:09
7

In my case we were dealing with PostgreSQL jsonb type in production and H2 for our tests.

I could not test @n00dle 's solution because apparently spring does not support executing a SQL script before Hibernate's ddl-auto=update for our tests so I used another way to solve this.

Here is a gist for it.

The overall idea is to create two package-info files. One for production and the other for tests and register different types (JsonBinaryType.class for production and TextType.class for tests) to handle them differently for PostgreSQL and H2

madz
  • 1,803
  • 18
  • 45
  • Mentioned gist suggest to remove `@Column(columnDefinition = "jsonb")` but why not leave it with change columnDefinition to `text` instead? – user9440008 Jan 09 '20 at 09:26
  • I cannot tell, because the result is unclear for me. But even if it works, I don't think having a text column type on the model field adds anything good! Its just making some misleading information! – madz Apr 05 '20 at 20:05
  • 1
    But without columnDefinition="jsonb" you can't create column in table if it doesn't exists. How do you solve it? – Hector Apr 20 '20 at 20:21
  • For the database, we were using db migrations so it was not our concern – madz Apr 21 '20 at 15:40
3

I have solved the problem using TEXT type in H2. One must create a separate database script to create schema in H2 for tests and replace the JSON type by TEXT.

It is still a problem since if you use Json function in queries, you will not be able to test those while with H2.

  • Unfortunatly, I'll have to use somethings else as I'm using the Spring @Type annotation which fails if I change JSON type to TEXT or CLOB into liquibase changelog file. – Alex May 24 '18 at 14:38
  • I would suggest that you deactivate the hibernate table creation for your test if you are using a different database type like H2. Use a spring profile to deactivate hibernate auto create and use a liquibase profile to use a different script for your test environnement. – Olivier Garand May 25 '18 at 15:24
  • You could also use Spring configurations with different profile to manage test entities vs prod entities and still using @Type and hibernate auto create. You then have to create "test" entities in a different package. in my opinion, it's a bad design and it's probably overkill and I would personnally deactivate autocreate and use custom liquibase scripts with different liquibase profiles. Hibernate table management is, again in my own opinion and experiences, too rigid to be used with multiple database. – Olivier Garand May 25 '18 at 15:28
  • 1
    Thanks for your replies :) I managed to replace H2 by EmbeddedPostgreSQL for running tests. It's a bit slower to startup, but it will prevent myself from future issues. Liquibase is not 100% DB agnostic, they are doing a wonderful job, but I had few issues when trying to use Oracle/Postgres switch and Postgres/H2. – Alex May 28 '18 at 08:36
2

Example with Kotlin + Spring + Hibernate + Postgres + jsonb column

Create the entity:

@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 comes from https://github.com/vladmihalcea/hibernate-types

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

Configure your H2 database in spring profile. The key line is this: 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

Write the test:

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

Alternatively you can try to define custom type per database in hibernate XML as described here: https://stackoverflow.com/a/59753980/10714479

klinec
  • 21
  • 1
0

I am in the same situation as @madz, where we use Postgres in production and H2 for unit tests. In my case i found a bit more simple solution, i think. We use Liquibase for database migrations, so here i made a conditional migration only to be run on H2, where i change the column type to H2's "other" type.

With the other type, H2 just stores it in the database and doesn't think twice about how the data is formatted etc. This does require however that you are not doing anything with the JSON directly in the database, and only in your application.

My migration looks like this:

  # 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

Along with this, i added the following to my test datasource:

INIT=create domain if not exists jsonb as text;
-1

The correct way of avoiding such things is using liquibase or flywaydb to evolve your schema and never ever allow Hibernate to create it.

Ilya Sazonov
  • 1,006
  • 10
  • 16
-2

H2 does not have the JSON data type.

In MySQL the JSON type is just an alias for the LONGTEXT data type so the actual data type for the column will be LONGTEXT.

Alex
  • 156
  • 2
  • 5