-
Notifications
You must be signed in to change notification settings - Fork 1
Local Files
What this page covers: opening the local-file backend with Storages.createLocalFile, its
one-arg LocalFileConfig (a base directory), the one-file-per-entity on-disk layout, the fact
that this backend accepts a non-JSON (YAML) codec (one of the two file backends that do — see
also Grouped Files), how queries work as a correct-but-slow full scan (no real index, yet
still validating index declarations), crash-safe atomic writes, and the two things it can't do
— no transactions, no index acceleration.
📌 Note — switching to local files is a one-line change at construction. From
storage.repository(...)onward your code is identical to every other backend. See Choosing a Backend for the full capability matrix.
import br.com.finalcraft.everydatabase.*;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
import br.com.finalcraft.everydatabase.modules.localfile.LocalFileConfig;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.UUID;
// 1. Describe the entity once — key type FIRST, entity type second.
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players") // becomes a sub-directory on disk
.keyExtractor(PlayerData::getUuid)
.codec(new JacksonJsonCodec<>(PlayerData.class)) // JSON here; YAML also allowed (below)
.build();
// 2. Open the backend — just point it at a directory.
LocalFileStorage storage = Storages.createLocalFile(
new LocalFileConfig(Paths.get("data")));
storage.init().join(); // creates the base directory if absent
// 3. Use it — exactly like any other backend.
Repository<UUID, PlayerData> repo = storage.repository(PLAYERS);
UUID id = UUID.randomUUID();
repo.save(new PlayerData(id, "Alice", 100)).join(); // writes data/players/<uuid>.json
Optional<PlayerData> alice = repo.find(id).join(); // -> Optional[Alice]
storage.close().join();createLocalFile returns the concrete LocalFileStorage type. The PlayerData entity is the same
plain Jackson POJO used throughout the wiki (Quick Start).
📌 Note — every I/O call returns a
CompletableFuture..join()is shown for brevity; compose withthenApply/thenComposein real code. There are no blocking variants — see The Async API.
One constructor — just the base directory:
import java.nio.file.Paths;
new LocalFileConfig(Paths.get("data"));| Argument | Type | Meaning |
|---|---|---|
baseDirectory |
Path |
root directory; each collection is a sub-directory under it |
💡 Tip — file formatting is the codec's job, not the config's. For readable JSON use
JacksonJsonCodec.pretty(Type.class)(the plainJacksonJsonCodecis compact);JacksonYamlCodecis inherently readable. See the YAML section below and Codecs.
Each collection is a sub-directory of the base directory; each entity is its own file named after its key:
data/
_schema_migrations.json # reserved: applied migration versions
players/
5f1e8400-e29b-41d4-a716-446655440000.json
9a2c…-….json
The filename is key.toString() plus the codec's file extension (.json / .yml). Path-separator
characters (/, \, :) are sanitised to _ to prevent directory traversal; when two distinct keys
would sanitise to the same name, a short hash of the original key is appended to keep one file per
key. An over-long key is hash-truncated — a short prefix plus a stable hash of the full key — so
it still maps to exactly one file and stays portable across filesystems. The key contract is otherwise
the usual one — a stable, unique toString() of ≤ 255 characters — see
Entities, Keys & Collections. Collection names must match
^[a-zA-Z][a-zA-Z0-9_]*$.
📌 Note — writes are crash-atomic. Each save goes to a sibling
.tmpfile and is then moved over the target withATOMIC_MOVE(falling back when the filesystem can't do an atomic move), so a crash mid-write never leaves a truncated entity file — at worst an orphan.tmp. This guards against torn files, not power loss: the write is not fsynced, so if the OS hasn't yet flushed its page cache, the very last save can still be lost on a power cut. Crash-atomic, not durable.
Local files treat the serialized payload as opaque bytes, so — together with Grouped Files —
this is one of the two backends that accept a non-JSON codec. Pair it with JacksonYamlCodec to get
human-friendly .yml files:
import br.com.finalcraft.everydatabase.codec.JacksonYamlCodec;
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.codec(new JacksonYamlCodec<>(PlayerData.class)) // -> data/players/<uuid>.yml
.build();# data/players/5f1e8400-….yml
uuid: "5f1e8400-e29b-41d4-a716-446655440000"
name: "Alice"
score: 100
⚠️ Gotcha —JacksonYamlCodecworks only on the file backends (here and Grouped Files). SQL, Mongo, and in-memory require a JSON codec (codec.isJsonCodec()) and throwIllegalArgumentExceptionfromrepository(...)if given YAML. If you later migrate a YAML local-file store into SQL, change the codec mid-transfer with the two-argdescriptor(srcDesc, dstDesc)form — see Moving Data Between Backends and Codecs.
For JSON, JacksonJsonCodec.pretty(Type.class) gives indented output (the plain JacksonJsonCodec
is compact).
Local files have no real index. Queries are answered by a full scan — every entity in the
collection is read and matched in memory. It's O(n) and slow on large collections, but it returns
the correct result, so a query that works here keeps working when you swap to SQL or Mongo.
repo.findBy("score", 100).join();
repo.query(Query.range("score", 50, null)).join(); // score >= 50 (full scan, correct result)Crucially, the backend still validates index declarations before scanning:
⚠️ Gotcha — querying a field that was not declared as anIndexHintthrowsIllegalArgumentExceptionhere too — even though the scan could answer it. This is deliberate: it stops a query that works on local files from silently breaking when the storage is swapped for SQL/Mongo (which genuinely require the declaration). Declare the field with.index(IndexHint.<type>("..."))or@Indexed. See Indexing & Queries.
Declare queryable fields the same way you would for any backend — the declaration keeps your data-access code portable, even though local files build no actual index from it.
LocalFileStorage does not implement tx.TransactionalStorage. There is no inTransaction.
⚠️ Gotcha — individualsaveoperations are atomic (the.tmp+ATOMIC_MOVEwrite), but there is no multi-entity transaction — you can't commit/rollback a group of writes as a unit. If you need ACID across multiple entities, use a SQL backend (MySQL & MariaDB / PostgreSQL / H2) or Mongo with a replica set. See Transactions.
Optimistic locking isn't enforced either (local files and in-memory don't enforce it — see Optimistic Locking and Choosing a Backend).
Unlike transactions, migrations work. LocalFileStorage implements schema.SchemaAwareStorage;
applied versions are tracked in a reserved _schema_migrations.json file at the base directory, and
migrations are forward-only. Extend LocalFileMigration and override
executeOnStorage(LocalFileStorage):
import br.com.finalcraft.everydatabase.modules.localfile.LocalFileMigration;
class V1_Seed extends LocalFileMigration {
@Override public String version() { return "001"; }
@Override public String description() { return "seed defaults"; }
@Override protected void executeOnStorage(LocalFileStorage storage) {
// mutate via repositories obtained from `storage`
}
}
storage.register(new V1_Seed()).migrate().join();MigrationContext.getNativeClient(...) also exposes the LocalFileStorage and its base Path. See
Schema Migrations.
🧭 Decision — choose local files when you want to read, diff, or hand-edit your data, or have no database server at all. One human-readable file per entity (YAML or pretty JSON), crash-atomic, zero ops. The trade-offs: no transactions, no real index (full-scan queries), no optimistic-locking enforcement — fine for small or cold datasets, configuration-like data, and single-server deployments. For large or write-heavy data, or anything needing ACID, prefer a SQL backend or Mongo.
A common pattern: ship small deployments on local files, let operators flip to MariaDB/Mongo for large ones and move the live data with Moving Data Between Backends — no code changes, source untouched.
- Choosing a Backend — the capability matrix and data-at-rest formats across all backends.
- Grouped Files — the key-major sibling (one file per key, every collection together).
- Quick Start — the minimal describe → open → use → close round-trip.
-
The Async API — the
CompletableFuturemodel every call shares. -
Codecs —
JacksonYamlCodec(file backends only) vs the JSON-required backends;prettyJSON. - Indexing & Queries — why declarations are validated even without a real index.
-
Schema Migrations —
LocalFileMigration,_schema_migrations.json, forward-only. - Entities, Keys & Collections — the key/collection contract and filename sanitisation.
- Transactions — why local files opt out, and the backends that don't.
- Cross-Process Cache Sync — no push feed in v1; version polling (deletes only).
- Moving Data Between Backends — migrate a YAML local-file store into SQL/Mongo (codec change).
EveryDatabase · Home · made by Petrus Pradella
Getting Started
Core Concepts
Working with Data
Backends
- Choosing a Backend
- MySQL & MariaDB
- PostgreSQL
- H2
- MongoDB
- Local Files
- Grouped Files
- In-Memory
- Benchmarks
Manager Module
- Caching & References
- Typed References (Ref)
- Caching Managers
- Cache Policies & Freshness
- Cross-Process Cache Sync
- Write-Back & Conflict Resolution
- Payload Schema Evolution
- One Entity, Many Databases
Operations
Advanced
Reference
Contributing