-
Notifications
You must be signed in to change notification settings - Fork 1
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/PoolTuningconfiguration with the other SQL dialects (its dialect uses double-quote identifiers, aTEXTdata column, andMERGE INTO ... KEY (...)upsert). See Choosing a Backend for the comparison and MySQL & MariaDB for pool tuning.
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 genericStorages.create(SqlConfig)builds a MySQL-dialectSqlStorage, not H2. Always callcreateH2explicitly. See Gotchas & Pitfalls.
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=PostgreSQLand usecreateH2. 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).
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()returnsfalse). 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.
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.
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()); aJacksonYamlCodecis rejected. Only Local Files accepts YAML. See Codecs.
| 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 |
- Choosing a Backend — the full capability matrix and decision guide.
- MySQL & MariaDB · PostgreSQL — the other SQL dialects and the shared pool tuning.
- Optimistic Locking — why H2 opts out and which backends enforce it.
- Transactions · Schema Migrations · Indexing & Queries — the capabilities H2 supports.
- Cross-Process Cache Sync — H2 has no push feed and (being unversioned) polls deletes only.
-
The Async API — the
CompletableFuturemodel. - Codecs — why a JSON codec is required here.
- Dependency Versions & Overrides — the H2 version, the Java-8 floor, and the 2.x override.
-
Gotchas & Pitfalls — the
create()dialect default, no-versioning, and the file-format trap.
EveryDatabase · Home · made by Petrus Pradella
Getting Started
Core Concepts
Working with Data
Backends
Manager Module
- Caching & References
- Typed References (Ref)
- Caching Managers
- Cache Policies & Freshness
- Cross-Process Cache Sync
- One Entity, Many Databases
Operations
Advanced
Reference
Contributing