-
Notifications
You must be signed in to change notification settings - Fork 1
CRUD Operations
What this page covers: the eight methods on Repository<K, V> —
find / findMany / save / saveAll / delete / exists / count / all — their exact
signatures, return shapes, and how to compose them asynchronously. Secondary-index reads
(findBy / query) have their own page: Indexing & Queries.
import br.com.finalcraft.everydatabase.*;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
import java.util.*;
// A descriptor (key type FIRST, entity type second) — see [[Defining Entities]].
EntityDescriptor<UUID, TestPlayer> PLAYERS = EntityDescriptor.builder(UUID.class, TestPlayer.class)
.collection("players")
.keyExtractor(TestPlayer::getUuid)
.codec(new JacksonJsonCodec<>(TestPlayer.class))
.build();
Repository<UUID, TestPlayer> repo = storage.repository(PLAYERS);
UUID id = UUID.randomUUID();
repo.save(new TestPlayer(id, "Alice", 100)).join(); // upsert
Optional<TestPlayer> found = repo.find(id).join(); // Optional — empty if absent
boolean exists = repo.exists(id).join(); // true
long total = repo.count().join(); // 1
boolean deleted = repo.delete(id).join(); // true (it existed)📌 Note — every method here returns a
CompletableFuture; there are no blocking variants. The examples call.join()only to stay short. In real code, compose withthenApply/thenCompose/thenAcceptinstead of blocking — failures surface as the cause of aCompletionException. The whole model is on The Async API.
TestPlayer mirrors the real test entity
(core/src/test/java/br/com/finalcraft/everydatabase/data/TestPlayer.java): a @Data
@NoArgsConstructor POJO with uuid / name / score. Any Jackson-serializable class with a
no-arg constructor works — see Defining Entities and Codecs.
| Method | Signature | Returns |
|---|---|---|
find |
find(K key) |
CompletableFuture<Optional<V>> — entity or Optional.empty()
|
findMany |
findMany(Collection<K> keys) |
CompletableFuture<List<V>> — missing keys omitted
|
save |
save(V entity) |
CompletableFuture<Void> — upsert (insert or replace) |
saveAll |
saveAll(Collection<V> entities) |
CompletableFuture<Void> — batched upsert |
delete |
delete(K key) |
CompletableFuture<Boolean> — true if it existed |
exists |
exists(K key) |
CompletableFuture<Boolean> |
count |
count() |
CompletableFuture<Long> — total stored |
all |
all() |
CompletableFuture<Stream<V>> — every entity |
Plus three members aimed at maintenance passes and cache sync rather than everyday CRUD:
| Method | Signature | Returns |
|---|---|---|
saveAll |
saveAll(Collection<V> entities, WriteMode mode) |
CompletableFuture<Void> — batch write under a mode |
scanAll |
scanAll(Cursor cursor, int limit) |
CompletableFuture<Slice<ScanRow<V>>> — key-ordered page |
versions |
versions(Collection<K> keys) |
CompletableFuture<Map<K, Long>> — key → lock_version, content-free |
The interface lives at core/.../Repository.java. Get one Repository per descriptor from a live
Storage; it's cached, so storage.repository(sameDescriptor) hands back the same object.
CompletableFuture<Optional<TestPlayer>> find(UUID key);A miss is an empty Optional, never a thrown exception. Compose off the future rather than
blocking:
repo.find(id)
.thenApply(opt -> opt.map(TestPlayer::getScore).orElse(0))
.thenAccept(score -> render(score));CompletableFuture<List<TestPlayer>> findMany(Collection<UUID> keys);Returns only the entities that exist. The result List can be shorter than the input
collection, and the order is not guaranteed to track the input order.
⚠️ Gotcha — because missing keys are omitted, you can't infer "which keys were absent" from the result size alone. If you need that, diff the returned entities' keys against the requested set:Set<UUID> requested = new HashSet<>(ids); List<TestPlayer> hits = repo.findMany(ids).join(); hits.forEach(p -> requested.remove(p.getUuid())); // 'requested' now holds the missing keys
CompletableFuture<Boolean> exists(UUID key);
CompletableFuture<Long> count();exists is cheaper than find(...).thenApply(Optional::isPresent) — backends answer it without
materializing the entity. count() is the total number of stored entities in the collection.
A row whose payload does not decode is counted — it occupies storage, and a later write to its
key overwrites it, so it exists. all() and query() skip it instead, which makes
count() != all().count() the signal that the collection holds a poisoned row rather than a bug;
scanAll is what names it.
⚠️ Gotcha (grouped files only) —count()andkeys()throw on a key file that cannot be parsed at all, instead of leaving it out. A key file there holds every collection sharing its key, so one that will not parse belongs to no collection: skipping it under-reports the collection being counted, counting it inflates every other collection in the directory. Neither number is true, so the call refuses and names the file. RunscanAllto list every offending file, then repair or remove it. This is not the poisoned row above — that file parses and declares the collection, so it is unambiguously a row of it and still counts. No other backend can reach this state. See Grouped Files.
CompletableFuture<Stream<TestPlayer>> all();The future yields a Stream that backends materialize internally (paginating large datasets);
consume it promptly:
long highScorers = repo.all().join()
.filter(p -> p.getScore() > 1000)
.count();💡 Tip —
all()is a full-collection read. For filtered reads, declare an index and usefindBy/queryinstead so the backend does the work — see Indexing & Queries.
CompletableFuture<Slice<ScanRow<TestPlayer>>> scanAll(Cursor cursor, int limit);Key-ordered, match-all pagination for a full-collection sweep. Unlike all() it doesn't materialise
the whole collection; unlike query it needs no declared index — it orders by the storage key
itself. Begin with Cursor.scan() and follow Slice.nextCursor() until hasNext() is false:
Cursor cursor = Cursor.scan();
while (true) {
Slice<ScanRow<TestPlayer>> page = repo.scanAll(cursor, 256).join();
for (ScanRow<TestPlayer> row : page.content()) {
if (row.isFailed()) { // ALWAYS check this first
log.warn("undecodable row {}: {}", row.key(), row.error());
continue;
}
process(row.value());
}
if (!page.hasNext()) break;
cursor = page.nextCursor().orElseThrow();
}The point is ScanRow. Every other read silently drops a row whose stored payload won't decode;
scanAll surfaces it (key + cause) so a maintenance pass can count it, name it in a log, and refuse
to declare the scan complete instead of mistaking a shorter scan for a finished one.
⚠️ Gotcha —scanAlldoes not spare you the decode. It still runs every row through the descriptor's codec and hands you aScanRow— what it avoids is materialising the whole collection and discarding failures silently, not the decoding itself. (That's deliberate: any decode-time transform on the codec, such as a payload-schema upcast, must run here too.)
⚠️ Gotcha — checkisFailed()beforevalue(). A failed row carries anullvalue and a non-nullerror(); a poisoned row straight intorow.value().getName()is an NPE. On a failed rowkey()is best-effort: backends that store the key separately (SQL, Mongo, InMemory) still report the real key, while the file backends fall back to the file name (which equals the key when it needed no sanitization).
📌 Note — every backend implements true per-backend pagination, but the single-instance file backends may return the whole collection in one page, treating
limitas advisory.
CompletableFuture<Map<UUID, Long>> versions(Collection<UUID> keys);A content-free read: key and version only, never the entity body (the file backends merely probe file/sub-document existence). It's the substrate the manager's polling cache sync uses to invalidate caches on backends without a native change feed — see Cross-Process Cache Sync.
⚠️ Gotcha — the returned number is only meaningful where the backend enforces versioning (MySQL/MariaDB, PostgreSQL, MongoDB). The file backends report a stamp derived from the file itself, which gives them update detection without a lock column. H2 reports0for every existing key — as does any non-versioned descriptor — so only presence/absence is reliable there: a poller can detect a deletion (a cached key missing from the result) but never an in-place update. In-memory reports whatever version is stored on the entity. Which backends enforce it is on Optimistic Locking.
CompletableFuture<Void> save(TestPlayer entity);save is an upsert: insert if the key is new, replace if it exists. The key is taken from the
entity by the descriptor's keyExtractor, then persisted by its toString().
repo.save(new TestPlayer(id, "Alice", 100)).join(); // insert
repo.save(new TestPlayer(id, "Alice", 250)).join(); // replace — same key, new score
⚠️ Gotcha — keys persist bytoString()and must be ≤ 255 characters. An oversized key isn't truncated; the returned future completes exceptionally withIllegalArgumentException, so the key can never silently collide. The full key contract (stable, uniquetoString(), valueequals/hashCode) lives on Entities, Keys & Collections.
If the descriptor opts into optimistic locking, a stale save fails with OptimisticLockException
instead of overwriting — see Optimistic Locking.
CompletableFuture<Void> saveAll(Collection<TestPlayer> entities);Same upsert semantics, but backends optimize the batch instead of N round-trips:
- SQL (MySQL/MariaDB/PostgreSQL/H2) — a single JDBC batch with a cached upsert-SQL string.
-
MongoDB — a
bulkWriteofReplaceOneModels, chunked at 1000 (non-versioned path). - LocalFile / GroupedFile / InMemory — entity-by-entity, but still through one call.
List<TestPlayer> roster = List.of(
new TestPlayer(UUID.randomUUID(), "Alice", 100),
new TestPlayer(UUID.randomUUID(), "Bob", 250),
new TestPlayer(UUID.randomUUID(), "Carol", 50));
repo.saveAll(roster).join();💡 Tip — prefer
saveAll(list)over a loop ofsave(...)whenever you're writing more than a couple of entities; on SQL and Mongo it collapses many round-trips into one.saveAllis not a transaction, though — for all-or-nothing semantics wrap it ininTransaction(...)on a transactional backend (see Transactions).
CompletableFuture<Void> saveAll(Collection<TestPlayer> entities, WriteMode mode);The default for every save/saveAll is WriteMode.UPSERT. The two-arg overload lets a maintenance
pass opt out of inserting:
| Mode | Key present | Key absent |
|---|---|---|
UPSERT (the default) |
replace | insert |
UPDATE_ONLY |
update | silent no-op — never inserts |
UPDATE_ONLY exists for passes that must never resurrect a row someone deleted concurrently — a
full-collection rewrite running while other work deletes rows. It's what the eager payload-schema
sweep writes with (see Payload Schema Evolution).
repo.saveAll(rewritten, WriteMode.UPDATE_ONLY).join(); // deleted-meanwhile keys stay deleted📌 Note —
UPDATE_ONLYkeeps the optimistic-lock guard on a versioned descriptor: a concurrent update still raisesOptimisticLockException(report it as a conflict), while a concurrent delete is absorbed as a no-op. The two are different events and stay distinguishable. Anullmode is treated asUPSERTand behaves exactly likesaveAll(Collection).
CompletableFuture<Boolean> delete(UUID key);Returns true if the entity existed, false if the key was already absent — a no-op delete is not
an error.
boolean removed = repo.delete(id).join();
if (!removed) {
// key wasn't there to begin with
}There is no batch-delete in the Repository contract; delete keys individually (compose the futures
if you have many):
CompletableFuture<Void> all = CompletableFuture.allOf(
ids.stream().map(repo::delete).toArray(CompletableFuture[]::new));
all.join();Because everything is a CompletableFuture, CRUD calls chain into pipelines that never block a
thread until you ask:
// "Load Alice, bump her score, save her back" — fully async.
repo.find(aliceId)
.thenApply(opt -> opt.orElseThrow(() -> new IllegalStateException("no Alice")))
.thenApply(alice -> { alice.setScore(alice.getScore() + 10); return alice; })
.thenCompose(repo::save) // CompletableFuture<Void>
.whenComplete((ok, err) -> {
if (err != null) log.warn("update failed", err);
});📌 Note — when you do block with
.join(), a failed future throws aCompletionExceptionwhosegetCause()is the real error (IllegalArgumentExceptionfor a bad key,OptimisticLockExceptionfor a version conflict, etc.). The Async API covers the error model in full.
-
The Async API — the
CompletableFuturemodel, composition, and exceptional completion. -
Indexing & Queries — filtered reads:
findBy,query, and declaring indexes. -
Defining Entities —
EntityDescriptor.builder, key extractor, collection rules. -
Entities, Keys & Collections — the key contract (
toString()≤ 255) and collection naming. - Codecs — how an entity becomes bytes; JSON vs YAML per backend.
-
Optimistic Locking — making
savefail loudly on a concurrent write; which backends enforceversions(). - Transactions — all-or-nothing writes across several calls.
-
Payload Schema Evolution — the maintenance sweep built on
scanAll+WriteMode.UPDATE_ONLY. -
Cross-Process Cache Sync — the poller
versions()feeds. - Choosing a Backend — where the data actually lives.
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