Skip to content
Petrus Pradella edited this page Jun 26, 2026 · 4 revisions

H2

What this page covers: creating an H2-backed Storage with Storages.createH2, the three H2 deployment modes (in-memory / embedded file / TCP server) selected by the JDBC URL, the deliberate no-optimistic-locking opt-out, the 1.x ↔ 2.x file-format incompatibility, and TEXT-column storage. H2 is the zero-ops embedded SQL backend — great for single-process apps and tests.

📌 Note — H2 shares the SqlConfig/PoolTuning configuration with the other SQL dialects (its dialect uses double-quote identifiers, a TEXT data column, and MERGE INTO ... KEY (...) upsert). See Choosing a Backend for the comparison and MySQL & MariaDB for pool tuning.


The smallest runnable example

import br.com.finalcraft.everydatabase.*;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
import br.com.finalcraft.everydatabase.modules.sql.SqlConfig;
import br.com.finalcraft.everydatabase.modules.sql.h2.H2SqlStorage;

EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
        .collection("players")
        .keyExtractor(PlayerData::getUuid)
        .codec(new JacksonJsonCodec<>(PlayerData.class))
        .build();

H2SqlStorage h2 = Storages.createH2(new SqlConfig("jdbc:h2:file:./data/storage", "", ""));
h2.init().join();

Repository<UUID, PlayerData> repo = h2.repository(PLAYERS);
repo.save(new PlayerData(id, "Alice", 100)).join();
Optional<PlayerData> alice = repo.find(id).join();

h2.close().join();

createH2 returns the concrete H2SqlStorage. Every call returns a CompletableFuture; .join() is shown for brevity — compose with thenApply/thenCompose in real code, there are no blocking variants. See The Async API.

⚠️ Gotcha — the generic Storages.create(SqlConfig) builds a MySQL-dialect SqlStorage, not H2. Always call createH2 explicitly. See Gotchas & Pitfalls.


Three modes, chosen by the JDBC URL

The same H2SqlStorage runs in all three H2 deployment modes — the URL decides which:

// In-memory (ephemeral — gone when the JVM exits, or when the last connection closes)
H2SqlStorage mem  = Storages.createH2(new SqlConfig("jdbc:h2:mem:test", "", ""));

// Embedded file (persists on disk, single JVM)
H2SqlStorage file = Storages.createH2(new SqlConfig("jdbc:h2:file:./data/storage", "", ""));

// Server / TCP (multiple JVMs share one H2 server)
H2SqlStorage tcp  = Storages.createH2(new SqlConfig("jdbc:h2:tcp://localhost:9092/./data/storage", "", ""));
Mode URL prefix Persistence Use for
In-memory jdbc:h2:mem: ephemeral tests, CI, throwaway state
Embedded file jdbc:h2:file: durable on disk single-process apps, zero-ops deployments
Server / TCP jdbc:h2:tcp:// durable on disk a few JVMs sharing one H2 server

💡 Tip — H2 is also useful as a PostgreSQL stand-in for tests: point it at jdbc:h2:mem:testdb;MODE=PostgreSQL and use createH2. Not a perfect dialect match, but it lets CI exercise the SQL path without a real server. See PostgreSQL.

The entity table is auto-created on first repository(...) and shares the SqlConfig/PoolTuning pool knobs with the other SQL dialects (see MySQL & MariaDB).


⚠️ No optimistic-locking enforcement (by design)

H2 is the one SQL dialect that does not enforce Optimistic Locking. A versioned descriptor (@OptimisticLock, .versioned(), or .version(...)) is perfectly legal on H2 — but it silently degrades to a plain upsert: the version is never checked, OptimisticLockException is never thrown, and creating the storage never fails because of versioning.

// This builds and runs fine on H2 — but the @OptimisticLock field is NOT enforced.
EntityDescriptor<UUID, Account> ACCOUNTS = EntityDescriptor.builder(UUID.class, Account.class)
        .collection("accounts")
        .keyExtractor(Account::getId)
        .codec(new JacksonJsonCodec<>(Account.class))
        .build();   // @OptimisticLock detected, but H2 won't enforce it

⚠️ Gotcha — H2 is an embedded/dev engine, so concurrent-writer protection is out of scope by design (supportsVersioning() returns false). If you rely on optimistic locking to coordinate multiple writers, use MySQL/MariaDB, PostgreSQL, or MongoDB — all of which enforce it. Local files and in-memory don't enforce it either. See Optimistic Locking.


⚠️ H2 1.x ↔ 2.x: incompatible file formats

The library pins H2 to 1.4.200 by default (the last Java-8-compatible release). H2 1.x and 2.x use incompatible database file formats and slightly different SQL dialects.

⚠️ Gotcha — never swap the H2 major version over an existing embedded-file database. The on-disk format isn't readable across the 1.x ↔ 2.x boundary — you must export and re-import instead. In-memory H2 (jdbc:h2:mem:) has no such concern (nothing persists). Running on Java 11+ and want H2 2.x? Override the dependency, but decide before going to production. The override recipe and exact version live on Dependency Versions & Overrides — don't hardcode versions elsewhere.


How data is stored

H2 stores the entity in a TEXT column (H2 1.x maps TEXT to CLOB, which can't be indexed — so the data column stays TEXT and is never indexed itself). Declared index fields (Indexing & Queries) get their own materialized columns instead: string indexes use VARCHAR (indexable at any length), timestamps use TIMESTAMP(3), each with a real index reconciled on repository open.

📌 Note — like every SQL backend, H2 requires a JSON codec (isJsonCodec()); a JacksonYamlCodec is rejected. Only Local Files accepts YAML. See Codecs.


Capabilities this backend supports

Capability H2
Transactions
Schema Migrations ✅ (tracked in _schema_migrations)
Indexing & Queries ✅ native column + index
Optimistic Locking silently degrades to upsert
Persistence file/tcp durable · mem ephemeral

See also

Clone this wiki locally