Hibernate In-memory Database for JUnit Tests with H2

Unit test Hibernate repositories against an in-memory H2 database with JUnit 6: one EntityManagerFactory per test class, data from import.sql, a fresh copy of the data before every test with truncate(), and the H2 settings and pitfalls that break tests.

logo

A Hibernate in-memory database test runs our entities and queries against a database such as H2 that keeps its tables and rows in the memory of the JVM, so it starts in milliseconds and is gone when the tests finish. Hibernate creates the tables from the entities, loads a few rows from import.sql, and runs the real SQL of our queries without a database server.

We use an in-memory database to unit test mappings, repositories and JPQL queries on every build, on a developer laptop or a CI agent that has no MySQL or PostgreSQL installed.

The following example configures Hibernate for an in-memory H2 database and adds the JUnit lifecycle methods that build one EntityManagerFactory per test class and give every test a fresh copy of the data.

EntityManagerFactory emf = new HibernatePersistenceConfiguration("invoices-test")
    .managedClasses(Invoice.class)
    .jdbcUrl("jdbc:h2:mem:invoices;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1")
    .jdbcCredentials("sa", "")
    .schemaToolingAction(Action.CREATE_DROP)
    .createEntityManagerFactory();          // create table Invoice (...)
                                            // + every insert from import.sql
@BeforeAll  static void createFactory() { emf = Database.inMemory(true); }   // create table + import.sql
@BeforeEach void resetData() { emf.getSchemaManager().truncate(); }          // truncate table + import.sql
@AfterEach  void closeEntityManager() { em.getTransaction().rollback(); }    // discard the test's changes
@AfterAll   static void closeFactory() { emf.close(); }                      // drop table if exists Invoice

Notice that the expensive factory is built once per class, whereas the cheap truncate() call runs before every test and reloads import.sql.

Next, we see what an in-memory test catches and build the invoice example step by step. The FAQs at the end cover the H2 surprises, such as vanishing tables and missing rows.

1. What Does an In-memory Database Test Catch?

A Hibernate test can run against a mock EntityManager that returns canned objects, against an in-memory database inside the JVM, or against the real database server. Only the last two execute SQL, so only they find a wrong column name or a broken JPQL query (JPQL is the object query language of Jakarta Persistence).

For example, a developer renames the field issuedOn to issueDate in the entity but forgets the JPQL query in findUnpaid(). A test with a mock EntityManager still passes, because no query runs. A test against H2 fails on the next build, because Hibernate cannot resolve issuedOn in the query.

QuestionMock EntityManagerIn-memory H2Real database (Testcontainers)
Runs real SQL?NoYes, H2’s SQLYes, production SQL
Finds mapping and JPQL errors?NoYesYes
Finds database-specific SQL errors?NoNo, H2 is not MySQLYes
Needs Docker or a server?NoNoDocker
Startup costNoneOne factory: 120 to 175 ms in our runsContainer start before the first test

H2 gives fast tests for mappings, repositories and JPQL queries, but it cannot prove that MySQL or PostgreSQL accepts our native SQL. We compare H2 and a real database in more detail in section 3.

2. Hibernate In-memory Database Test Example

The following example is a small invoicing app for a freelancer, built with Hibernate 7.4, Java 25, H2 2.5 and JUnit 6. An Invoice has a client, an amount, an issue date and a paid flag, and an InvoiceRepository reads and changes invoices.

2.1. Maven Dependencies

Only the tests need H2, so it goes in the test scope next to JUnit. The JUnit BOM (bill of materials, a pom that only lists versions) keeps all JUnit modules on the same version.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>6.1.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.4.11.Final</version>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.5.252</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

2.2. The Entity and the Repository

The Invoice class is a plain @Entity with a generated ID. The amount is a BigDecimal and the issue date a LocalDate.

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

@Column(nullable = false)
private String client;

@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;

@Column(name = "issued_on", nullable = false)
private LocalDate issuedOn;

private boolean paid;

The repository gets an EntityManager in its constructor. Constructor injection makes the repository testable, because the test decides which database and which transaction the repository uses.

public Invoice save(Invoice invoice) {
  em.persist(invoice);
  return invoice;
}

public List<Invoice> findUnpaid() {
  return em.createQuery("from Invoice where paid = false order by issuedOn", Invoice.class)
      .getResultList();
}

public BigDecimal outstandingFor(String client) {
  return em.createQuery(
          "select coalesce(sum(amount), 0) from Invoice where client = :client and paid = false",
          BigDecimal.class)
      .setParameter("client", client)
      .getSingleResult();
}

public int markAllPaid(String client) {
  return em.createQuery("update Invoice set paid = true where client = :client")
      .setParameter("client", client)
      .executeUpdate();
}

2.3. Configuring Hibernate for H2

The JDBC URL decides how H2 behaves. The prefix mem: makes it an in-memory database, and the settings after each semicolon change its behavior.

The URL jdbc:h2:mem:invoices;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1 split into four parts: mem:invoices keeps the database in the JVM heap; MODE=MySQL accepts more MySQL syntax and restarts IDs on TRUNCATE but date_format() still fails; DATABASE_TO_LOWER=TRUE stores the table as invoice; DB_CLOSE_DELAY=-1 keeps the database until the JVM stops
MODE=MySQL brings H2 closer to MySQL, but it does not turn H2 into MySQL.

The class HibernatePersistenceConfiguration builds the factory in Java, without a persistence.xml file. The value Action.CREATE_DROP drops and creates the tables when the factory starts and drops them again when it closes, the same as hibernate.hbm2ddl.auto=create-drop.

public static final String H2_URL =
    "jdbc:h2:mem:invoices;MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1";

public static HibernatePersistenceConfiguration h2(String url) {
  return new HibernatePersistenceConfiguration("invoices-test")
      .managedClasses(Invoice.class)
      .jdbcUrl(url)
      .jdbcCredentials("sa", "")
      .schemaToolingAction(Action.CREATE_DROP);
}

public static EntityManagerFactory inMemory(boolean showSql) {
  return h2(H2_URL)
      .showSql(showSql, false, false)
      .createEntityManagerFactory();
}
drop table if exists Invoice cascade
create table Invoice (amount numeric(10,2) not null, issued_on date not null, paid boolean not null,
  id bigint generated by default as identity, client varchar(255) not null, primary key (id))

Older tutorials build a SessionFactory from a hibernate-test.cfg.xml file with StandardServiceRegistryBuilder. HibernatePersistenceConfiguration (new in Hibernate 7) does the same job in a few lines of Java and returns a standard Jakarta Persistence factory.

2.4. Loading Test Data With import.sql

Hibernate runs the file import.sql from the root of the classpath right after it creates the tables. In a Maven project, the file goes to src/test/resources/import.sql.

insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 400.00, '2026-09-01', true);
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 250.00, '2026-09-15', false);
insert into invoice (client, amount, issued_on, paid) values ('Alex', 120.00, '2026-09-20', false);

Hibernate has more than one way to pick a load script, and the options combine in different ways. We loaded the same one-row file extra-invoices.sql (an invoice for Maria) with each setting, and the last column shows which rows were in the table.

SettingStandard?Rows after startup
None, import.sql on the classpathHibernate onlyLokesh, Lokesh, Alex
jakarta.persistence.sql-load-script-source = extra-invoices.sqlJakarta PersistenceMaria, Lokesh, Lokesh, Alex (both files run)
hibernate.hbm2ddl.import_files = extra-invoices.sqlHibernate onlyMaria (replaces import.sql)
EntityManagerFactory emf = Database.h2(Database.H2_URL)
    .property("jakarta.persistence.sql-load-script-source", "extra-invoices.sql")
    .createEntityManagerFactory();

Hibernate reads load scripts one line per statement. An insert split over two lines fails with a syntax error that Hibernate only logs as a warning, so the test starts with missing rows. The fix is in FAQ 4.3.

2.5. One EntityManagerFactory per Test Class, Fresh Data per Test

Building an EntityManagerFactory is the slow part, because Hibernate reads the mappings and creates the tables. So we build it once in @BeforeAll and close it in @AfterAll. Each test still needs the same starting data, so the @BeforeEach method empties the tables and loads import.sql again.

JUnit lifecycle with an in-memory H2 database: @BeforeAll creates the EntityManagerFactory with create table and import.sql; for every test @BeforeEach runs truncate table and import.sql again and begins a transaction, @Test calls the repository, @AfterEach rolls back and closes the EntityManager; @AfterAll closes the factory with drop table
The expensive work runs once per class. truncate() gives every test the same three invoices.

The call emf.getSchemaManager().truncate() is new in Jakarta Persistence 3.2. It truncates the mapped tables and re-imports the initial data from the configured load scripts, and the SQL log shows Hibernate doing both.

private static EntityManagerFactory emf;
private EntityManager em;
private InvoiceRepository repository;

@BeforeAll
static void createFactory() {
  emf = Database.inMemory(true);                   // create tables + run import.sql
}

@BeforeEach
void resetData() {
  emf.getSchemaManager().truncate();               // empty tables + run import.sql again
  em = emf.createEntityManager();
  em.getTransaction().begin();
  repository = new InvoiceRepository(em);
}

@AfterEach
void closeEntityManager() {
  if (em.getTransaction().isActive()) {
    em.getTransaction().rollback();
  }
  em.close();
}

@AfterAll
static void closeFactory() {
  emf.close();                                     // drop tables
}
set referential_integrity false
truncate table Invoice
set referential_integrity true
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 400.00, '2026-09-01', true)
insert into invoice (client, amount, issued_on, paid) values ('Lokesh', 250.00, '2026-09-15', false)
insert into invoice (client, amount, issued_on, paid) values ('Alex', 120.00, '2026-09-20', false)

The fields emf and em follow the JUnit rules. @BeforeAll methods are static by default, so emf is a static field, whereas JUnit creates a new test class instance, with new instance fields, for every test method, so the @BeforeEach method fills em each time.

2.6. Testing the Repository

Each test calls the repository and checks the result against the three invoices from import.sql. Because truncate() reloads the same rows before every test, the tests can run in any order.

List<Invoice> unpaid = repository.findUnpaid();
// [Lokesh 250.00 (open), Alex 120.00 (open)]
// select ... from Invoice i1_0 where i1_0.paid=false order by i1_0.issued_on

Invoice invoice = repository.save(new Invoice("Maria", "90.00", LocalDate.of(2026, 9, 25)));
Long id = invoice.getId();                                 // 4
// insert into Invoice (amount,client,issued_on,paid,id) values (?,?,?,?,default)

BigDecimal lokesh = repository.outstandingFor("Lokesh");   // 250.00
BigDecimal maria = repository.outstandingFor("Maria");     // 0 (no rows, coalesce)

int updated = repository.markAllPaid("Lokesh");            // 2
// update Invoice i1_0 set paid=true where i1_0.client=?
BigDecimal after = repository.outstandingFor("Lokesh");    // 0

Changes inside a test stay invisible to the next one, even when a test commits. In the following test, we delete every invoice and commit, and the next test still starts with three rows.

em.createQuery("delete from Invoice").executeUpdate();
em.getTransaction().commit();
Long count = em.createQuery("select count(*) from Invoice", Long.class).getSingleResult();   // 0
// next test: @BeforeEach truncates and reloads, count = 3

Besides the repository tests, the complete project on GitHub has 12 more tests that check the H2 behavior from the FAQs (18 tests in total) and a demo class that prints the SQL of each lifecycle step (mvn test, mvn -q test-compile exec:java).

2.7. Resetting Data Between Tests

The method truncate() is not the only way to give each test clean data. We can also build a new factory for every test, or roll back each test’s transaction. The strategies differ in speed and in what they clean up.

StrategyHowCost per test (our runs)Cleans up committed data?
New factory per testFactory in @BeforeEach, create-drop120 to 175 msYes, tables are dropped
truncate() per testOne factory, truncate() in @BeforeEach9 to 12 msYes, rows are reloaded
Rollback per testOne factory, rollback() in @AfterEachNo extra SQLNo, see FAQ 4.6

The times come from two runs of the demo class (10 rounds of each strategy per run) and only show the order of magnitude. We use one factory per class with truncate() and keep the rollback as a second safety net, which is the setup in section 2.5.

3. H2 vs Testcontainers

Testcontainers starts the real database in a Docker container for the tests. It runs the same SQL as production, so it catches the errors H2 hides. The cost is Docker on every developer machine and CI agent, plus a container start before the first test. Say the invoice app later adds a monthly report that uses a MySQL window function in a native query. H2 cannot prove that query works, whereas a Testcontainers test runs it on MySQL 8.4.

PointIn-memory H2Testcontainers
SetupOne test dependencyDocker plus a module such as testcontainers-mysql
SpeedStarts inside the JVMContainer start first, then normal speed
SQL dialectH2, with a compatibility modeThe production database and version
Native queries, functions, proceduresFail or behave differentlySame as production
Good forMappings, repositories, JPQL, fast feedbackNative SQL, database features, final integration tests

With the Testcontainers JDBC driver on the test classpath, only the URL changes. The Testcontainers configuration needs Docker, so it is not part of the example project.

EntityManagerFactory emf = new HibernatePersistenceConfiguration("invoices-test")
    .managedClasses(Invoice.class)
    .jdbcUrl("jdbc:tc:mysql:8.4:///invoices")    // Testcontainers starts mysql:8.4 in Docker
    .schemaToolingAction(Action.CREATE_DROP)
    .createEntityManagerFactory();

Many projects use both. They run H2 for the fast unit tests of repositories, and Testcontainers for the classes that use native SQL or stored procedures.

4. In-memory Database Testing FAQs

4.1. Why Does My H2 Table Disappear Between Connections?

By default, H2 deletes an in-memory database when its last connection closes. A table created over one connection is gone when the next connection opens, and H2 reports that the database is empty.

-- connection 1
create table invoice (id int);      -- then close the connection
-- connection 2
select * from invoice;
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "INVOICE" not found (this database is empty); SQL statement:
select * from invoice [42104-252]

The setting DB_CLOSE_DELAY=-1 keeps the database until the JVM stops, and the same two connections then find the table. Hibernate’s built-in connection pool keeps at least one connection open while the factory lives (Hibernate logs Minimum pool size: 1 at INFO level when the factory starts), so DB_CLOSE_DELAY=-1 matters most for test code that opens its own JDBC connections.

4.2. Why Does a Native Query Fail on H2 but Work on MySQL?

H2 accepts much of MySQL’s syntax in MODE=MySQL, but it does not have every MySQL function. For example, the following native query uses the MySQL function DATE_FORMAT().

List<?> months = em.createNativeQuery("select date_format(issued_on, '%Y-%m') from invoice").getResultList();   // SQLGrammarException
org.hibernate.exception.SQLGrammarException: Could not prepare statement [Function "date_format" not found; SQL statement:
select date_format(issued_on, '%Y-%m') from invoice [90022-252]]

When the query can be written in JPQL instead, Hibernate generates the SQL for the configured dialect, so the query needs no database-specific function.

List<Object[]> totals = em.createQuery("""
    select extract(month from issuedOn), sum(amount)
    from Invoice group by extract(month from issuedOn)""", Object[].class)
  .getResultList();                               // [[9, 770.00]]

A native query that must stay native belongs in a Testcontainers test.

4.3. Why Are the Rows From import.sql Missing?

The default script reader (SingleLineSqlScriptExtractor) treats every line as one statement. A statement that spans two lines is cut at the line break, H2 rejects the first half, and Hibernate only logs a warning.

insert into invoice (client, amount, issued_on, paid)
values ('Maria', 90.00, '2026-09-25', false);
WARN org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl - GenerationTarget encountered exception accepting command : Error executing DDL "insert into invoice (client, amount, issued_on, paid)" via JDBC [Syntax error in SQL statement "insert into invoice (client, amount, issued_on, paid)[*]"; ...]

We either keep each statement on one line, or switch to the multi-line reader.

.property("hibernate.hbm2ddl.import_files_sql_extractor",
    "org.hibernate.tool.schema.internal.script.MultiLineSqlScriptExtractor")

4.4. Why Do IDs Not Start at 1 After Each Test?

H2’s TRUNCATE TABLE keeps the identity counter in its regular mode. The rows come back, but with new IDs. In MODE=MySQL, H2 restarts the counter, as MySQL does.

URLIDs after truncate()
jdbc:h2:mem:invoices;MODE=MySQL;…1, 2, 3
jdbc:h2:mem:regular;DB_CLOSE_DELAY=-14, 5, 6

Tests that look up rows by a fixed ID depend on this setting, so we either use MODE=MySQL or find the rows by a business value such as the client name.

4.5. Why Does persist() Fail With a Primary Key Violation After Loading Data?

The load script inserted a row with a fixed ID, but H2’s identity counter did not move. The next em.persist() gets ID 1 again and fails.

insert into invoice (id, client, amount, issued_on, paid) values (1, 'Maria', 90.00, '2026-09-25', false);
org.hibernate.exception.ConstraintViolationException: could not execute statement [Unique index or primary key violation: "PUBLIC.CONSTRAINT_9 PRIMARY KEY ON PUBLIC.INVOICE(ID) ( /* key:1 */ 90.00, DATE '2026-09-25', FALSE, CAST(1 AS BIGINT), 'Maria')"; SQL statement:
insert into Invoice (amount,client,issued_on,paid,id) values (?,?,?,?,default) [23505-252]]

With MODE=MySQL, H2 moves the counter past an inserted ID, and the next invoice gets ID 2. The cleanest fix in any mode is to leave the id column out of the load script, as our import.sql does.

4.6. Is Rolling Back the Transaction After Each Test Enough?

No, the rollback is enough only when every change goes through the test’s own transaction. When the code under test commits in its own transaction, for example with emf.runInTransaction(), the rollback in @AfterEach cannot undo that commit.

em.getTransaction().begin();
emf.runInTransaction(other ->                      // code under test commits on its own
    other.persist(new Invoice("Maria", "90.00", LocalDate.of(2026, 9, 25))));
em.getTransaction().rollback();
// Maria is still in the table: 4 invoices

For that reason our setup also truncates in @BeforeEach, so the next test starts with the three rows from import.sql, whatever the previous test committed.

5. Conclusion

An in-memory H2 database lets us unit test Hibernate mappings, repositories and JPQL queries with real SQL and without a database server. We build one EntityManagerFactory per test class, load the data from import.sql, call truncate() before each test, and pick the H2 URL settings on purpose, such as DB_CLOSE_DELAY=-1 to keep the database and MODE=MySQL to get closer to MySQL. For native SQL and database features, a Testcontainers test against the real database is the right tool.

6. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I believe you may have an erroneous “1” in the password
    hibernate.connection.password”>1

    When I removed it, it worked.

    Thanks none-the-less!

  2. here is an error in hibernate.cfg.xml
    org.hsqldb.jdbcDriver
    The name of class is supposed to start with capital later

  3. Hello Lokesh,

    I am wondering why select query is not working in my case.
    I mean when i write code to select all data from the data base or say row,It is just executing the select query but list size is 0.why so? your example is show only insert operation.please give me brief about this to how to do it.

  4. Thanks Lokesh, Its very helpful. I just came to know that, we can use hibernate to connect IM database like any other RDBMS by this tutorial.
    I have a couple of questions here.
    #1. Do we need to install HSql DB separetly? How ? (Some info links would be helpful :))
    #2. If i want to use this HSQL data base(or any other IM db) for my Junits, Do i need to dump all my database objects and testdata into this database? Or any other way i can use existing DB as IM DB?

    Why I am asking this is, we are working in a project where most of the business logic reside at DB Procedures. I am thinking in a way how can we make our junits without depends on our database(Oracle).

    Thanks in Advance. Please Help.

    • 1) For in memory usage, you do not need to install HSQLDB. Just include its jar file as I have added through maven.
      2) I guess you will need to dump all your data before execution. It’s in-memory so once it’s not in use, it will be destroyed including DB schema.

      If you not want to use external database, then recommended practice is to use mocking.

      • Thanks again lokesh.

        Do u have any idea, what are all other IM databases which is supporting by hibernate4?

        Help me with some tutorial links to explore on Hsql db.

  5. Thanks for very helpful tutorial. I’m new in Java and Hibernate as well. I managed to compile the code and understood the concept as well. But the problem is that I don’t know how to execute the code to debug some stuff. When I start app as java application in eclipse. I have a long list to of option. But I don’t see my actual test class to run. So How to run it and debug this app? Please see the link I asked same question in the stackoverflow @ https://stackoverflow.com/questions/26563177/using-in-memory-database-with-hibernate-tutorial-how-to-execute

      • hi guys,

        i am using Hibernate 4.3.5
        i followed the tutorial,
        i can see on the logs that the tables are created
        but i am having this error on the first insert.

        “java.sql.SQLSyntaxErrorException: user lacks privilege or object not found: ADVISORID

        did anyone faced the same error ?
        thanks

          • thank you for the reply,

            in fact , the problem was a definition for a certain column on my entity

            // [Causing problem] 
            @Column(name = "ADV_LANGUAGE", nullable = false, columnDefinition = "CHAR(3) NOT NULL DEFAULT 'FR'")
                private String lang;
            
            // [Fixing problem] 
            @Column(name = "ADVT_LANGUE", nullable = false, length = 3)
               private String lang;
            

            thank you for this great tuto, helped a lot.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.