-
Notifications
You must be signed in to change notification settings - Fork 1
Entities Keys and Collections
What this page covers: the three things a backend needs to model your data portably — a valid collection name, a well-behaved key, and a serializable entity — and the rules each must satisfy so the same code behaves identically on SQL, Mongo, files, and memory.
import br.com.finalcraft.everydatabase.*;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
// A plain entity: no-arg constructor + accessors so the codec can (de)serialise it.
public class PlayerData {
private UUID uuid;
private String name;
private int score;
public PlayerData() {} // Jackson needs this
public PlayerData(UUID uuid, String name, int score) { /* ... */ }
public UUID getUuid() { return uuid; } // the key
// getters/setters omitted
}
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players") // ^[a-zA-Z][a-zA-Z0-9_]*$
.keyExtractor(PlayerData::getUuid) // UUID: a great key
.codec(new JacksonJsonCodec<>(PlayerData.class))
.build();The descriptor binds all three together. UUID keys, a players collection, and a Jackson-
serializable POJO are the happy path — the rest of this page is the why and the edges.
📌 Note —
EntityDescriptor.builder(...)takes the key type first, entity type second. See Defining Entities for the full builder and Architecture Overview for where the descriptor sits.
A collection is one logical bucket — a SQL table, a Mongo collection, a LocalFile directory. Its
name is validated at build() time against:
^[a-zA-Z][a-zA-Z0-9_]*$
- must start with a letter (
a–z/A–Z); - the rest may be letters, digits, or underscores (
_); - no spaces, hyphens, dots, backticks, quotes, or any other special character.
This is the intersection of safe identifiers across every backend — it needs no quoting or
escaping in SQL (any dialect), is a legal Mongo collection name, and is a legal directory name. A
name that breaks the rule throws IllegalStateException from build():
.collection("player-data") // ✗ IllegalStateException: hyphen
.collection("2players") // ✗ IllegalStateException: starts with a digit
.collection("player_data") // ✓
⚠️ Gotcha — the validation fires atbuild(), not atrepository(...). A bad collection name fails fast when you construct the descriptor, which is usually at class-load time for astatic finalconstant — so you find out immediately, not on first I/O.
A key is persisted by its toString() — the SQL primary key (storage_key VARCHAR(255)), the
Mongo unique index, the LocalFile filename — and matched by equals/hashCode (the in-memory
backend and the manager cache look keys up by value). For a key to behave identically everywhere,
its type must have:
-
a stable, unique
toString()of at most 255 characters, and -
value-based
equals/hashCode.
These types qualify out of the box:
| Key type | Why it works |
|---|---|
UUID |
canonical 36-char toString(), value equality — the idiomatic player key |
String |
is its own toString(), value equality |
Long / Integer
|
numeric toString(), value equality |
a record
|
compiler-generated value toString()/equals/hashCode — great for composite keys |
// A composite key as a record: stable toString(), value equality — qualifies.
public record GuildMember(UUID guildId, UUID playerId) {}
EntityDescriptor.builder(GuildMember.class, Membership.class) /* ... */;
⚠️ Gotcha — a key type with the defaultObject.toString()(the identity hash likeFoo@1a2b3c) is poison: it's neither stable across runs nor meaningful, so it silently corrupts persistence. If you key by a custom class, give it a realtoString()and valueequals/hashCode. Arecordgives you all three for free — prefer it.
A key whose toString() exceeds 255 characters (StorageKeys.MAX_KEY_LENGTH) is refused before
it reaches storage: save/saveAll return a future that completes exceptionally with
IllegalArgumentException.
String huge = "x".repeat(300);
repo.save(new Thing(huge, ...))
.exceptionally(ex -> {
// ex.getCause() instanceof IllegalArgumentException — "Storage key ... is too long: 300 ..."
return null;
});This is deliberate. 255 is the safe intersection across backends (the SQL column width, within Mongo's index-key limit and the filesystem filename limit). Failing fast beats the alternatives: an opaque "Data too long" / "File name too long" later, or — worst, on SQL — a silent truncation that collides with a different key. Reads aren't validated: an oversized key simply matches nothing.
📌 Note — the rejection is an exceptional future, not a synchronous throw, matching the library's error-propagation contract. See The Async API.
💡 Tip — when a key is used inside a
Ref(the Caching & References) it additionally has to be JSON-(de)serializable, since theRefpersists as just that key.UUID,String,Long,Integer, andrecords all satisfy that too.
The entity is whatever your Codec can round-trip. With the default JacksonJsonCodec, that means
a standard Jackson-serializable POJO:
- a no-arg constructor (or a Jackson-recognised creator) so the codec can rebuild instances on read;
- accessible fields — getters/setters, or Lombok
@Data, or public fields.
// Lombok keeps it terse and is used throughout the tests.
@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor
public class PlayerData {
private UUID uuid;
private String name;
private int score;
}The key field is part of the entity — keyExtractor just reads it (PlayerData::getUuid).
You don't store the key separately; the backend derives it from the entity at save time.
How the serialized payload lands at rest depends on the backend:
| Backend | Entity stored as |
|---|---|
| MySQL / MariaDB | native JSON column |
| PostgreSQL | native JSON column |
| MongoDB | native BSON sub-document |
| H2 | TEXT column (JSON text) |
| LocalFile | one file per entity (JSON or YAML bytes) |
| InMemory | parsed JSON, in a map |
The codec choice and a backend's storage format are linked: most backends require a JSON codec; only LocalFile accepts YAML. That seam is its own page — see Codecs.
📌 Note — the entity's indexed fields are a separate concern: declare them with
@IndexedorIndexHinton the descriptor and the backend materialises queryable index columns/fields alongside the payload. See Indexing & Queries.
// Collection name, key contract, and a JSON-serializable entity — all validated at build().
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players") // valid identifier
.keyExtractor(PlayerData::getUuid) // UUID key: stable, unique, value-equal
.codec(new JacksonJsonCodec<>(PlayerData.class))// JSON: works on every backend
.build();
Repository<UUID, PlayerData> repo = storage.repository(PLAYERS);
repo.save(new PlayerData(UUID.randomUUID(), "Alice", 100)).join();Mirror real tested entity shapes when in doubt — TestPlayer in the core test sources is the
canonical reference for a keyed, indexed entity.
-
Defining Entities — the full
EntityDescriptor.builderwalkthrough. - Codecs — choosing a serialization format and the JSON-vs-YAML backend seam.
- Architecture Overview — where descriptors, storages, and repositories fit.
- The Async API — why the oversized-key rejection arrives as an exceptional future.
- Indexing & Queries — declaring queryable fields on the descriptor.
- CRUD Operations — using the repository the descriptor produces.
-
Caching & References — keys inside a
Ref(the manager add-on).
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