-
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 here 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 |
The interface lives at core/.../Repository.java. Get one Repository per descriptor from a live
Storage; the storage caches it, 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. Idiomatic handling composes 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 a cheaper "is this key present?" than find(...).thenApply(Optional::isPresent) —
backends answer it without materializing the entity. count() is the total number of stored
entities in the collection.
CompletableFuture<Stream<TestPlayer>> all();The future yields a Stream that backends materialize internally (paginating large datasets), so
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<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 issuing 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 / 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<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 them to:
// "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. - Transactions — all-or-nothing writes across several calls.
- Choosing a Backend — where the data actually lives.
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