-
Notifications
You must be signed in to change notification settings - Fork 1
Defining Entities
What this page covers: the EntityDescriptor — the single piece of metadata that tells a backend
how to model your entity: its collection name, how to extract the key, how to (de)serialize it, and
(later) its indexes and version field. Get this right once and every backend understands your entity.
📌 Note — an
EntityDescriptoris immutable and backend-agnostic. You build one per entity type and hand the same descriptor to any storage. Repositories are cached per collection name, sostorage.repository(sameDescriptor)returns the same object.
import br.com.finalcraft.everydatabase.EntityDescriptor;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
import java.util.UUID;
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.codec(new JacksonJsonCodec<>(PlayerData.class))
.build();Four required pieces: the types (in builder), the collection, the keyExtractor, and the
codec. build() validates them and fails fast if any is missing or invalid.
⚠️ Gotcha —builder(Class<K> keyType, Class<V> type)takes the key type FIRST. Both parameters areClass<?>, so a flip compiles and only blows up at runtime. The real signature is:public static <K, V> Builder<K, V> builder(Class<K> keyType, Class<V> type)Key first (
UUID.class), entity second (PlayerData.class).
| Method | Required? | What it sets |
|---|---|---|
builder(keyType, type) |
✅ | the Class<K> key type and Class<V> entity type (key first) |
.collection(String) |
✅ | the logical collection/table/directory name (validated — see below) |
.keyExtractor(Function<V, K>) |
✅ | how to read an entity's key |
.codec(Codec<V>) |
✅ | the serialization strategy |
.index(IndexHint) |
optional | a manual secondary index (see Indexing & Queries) |
.version(getter, setter) / .versioned()
|
optional | optimistic locking (see Optimistic Locking) |
.build() |
✅ | validates, scans annotations, returns the immutable descriptor |
build() does more than assemble fields: it scans the entity class for @Indexed and
@OptimisticLock annotations and merges them in. Those two are covered on Indexing & Queries and
Optimistic Locking; this page focuses on the four essentials.
The collection is the logical name of the table (SQL), collection (Mongo), or directory (local
files). It is validated at build() against:
^[a-zA-Z][a-zA-Z0-9_]*$
Start with a letter; the rest may be letters, digits, or underscores. No spaces, hyphens, dots, backticks, or quotes. This is the safe intersection of identifier rules across every supported backend, so a valid name never needs quoting or escaping anywhere.
.collection("players") // ✅
.collection("guild_members") // ✅
.collection("audit_log_2024") // ✅
.collection("guild-members") // ❌ hyphen -> IllegalStateException at build()
.collection("2fa") // ❌ leading digit
.collection("user.profile") // ❌ dotA bad name throws IllegalStateException from build() with a message telling you the rule.
The key identifies an entity. It is persisted two ways, and a key type must satisfy both:
- by its
toString()— the SQL primary key, the Mongo unique index, the LocalFile filename; - by
equals/hashCode— the in-memory backend and the manager cache.
So a key type needs a stable, unique toString() of at most 255 characters and value-based
equals/hashCode. These all qualify out of the box:
-
UUID,String,Long,Integer,Boolean - any
record(value-based by definition)
The default identity Object.toString() (e.g. com.example.Foo@1b6d3586) does not — it's
neither stable across runs nor value-based.
⚠️ Gotcha — an oversized key is rejected up front, never silently truncated. If a key'stoString()exceeds 255 characters,save/saveAllreturn a future that completes exceptionally withIllegalArgumentException— the key never reaches storage, so it can't be truncated into a collision.
💡 Tip — the
keyExtractorjust reads the key off an instance; it does not have to be a field. A compositerecordkey works fine:public record SettingKey(String scope, String name) {} // ... .keyExtractor(s -> new SettingKey(s.getScope(), s.getName()))The whole key contract (the 255-char limit, the dual persistence) is also detailed on Entities, Keys & Collections.
The codec turns your entity into bytes and back. With the default JacksonJsonCodec, your entity
must be Jackson-serializable (Jackson 2.x, com.fasterxml.jackson.*). In practice that means:
- a no-arg constructor (or an appropriate
@JsonCreator), and - accessors Jackson can use (getters/setters), or Jackson annotations describing the mapping.
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) {
this.uuid = uuid; this.name = name; this.score = score;
}
public UUID getUuid() { return uuid; }
public String getName() { return name; }
public int getScore() { return score; }
// setters...
}Lombok works well here — @Data @NoArgsConstructor @AllArgsConstructor gives you exactly the shape
above. (The test entities and the manager examples are written this way.)
| Codec | Output | Use it for |
|---|---|---|
new JacksonJsonCodec<>(Type.class) |
compact JSON | the default — smallest payload, what a database wants |
JacksonJsonCodec.pretty(Type.class) |
indented JSON | human-readable LocalFile output |
new JacksonYamlCodec<>(Type.class) |
YAML |
LocalFile only — eyeball-friendly .yml files |
⚠️ Gotcha — SQL, Mongo, and InMemory require a JSON codec (they parse/store the payload as native JSON). Only LocalFile accepts a non-JSON codec likeJacksonYamlCodec. Attaching a YAML codec to a SQL descriptor is rejected. The deciding signal isCodec.isJsonCodec(); the full story — content types, custom codecs — is on Codecs.
💡 Tip —
JacksonJsonCodec.pretty(...)on a LocalFile backend gives you indented JSON files you can read and diff by hand, without leaving JSON for YAML.
Because a descriptor is immutable and engine-neutral, the same PLAYERS constant drives every
backend — that's what makes the one-line backend swap in Quick Start possible, and what lets
Moving Data Between Backends copy a collection from one engine to another by handing the transfer
the same descriptor.
Repository<UUID, PlayerData> onSql = sqlStorage.repository(PLAYERS);
Repository<UUID, PlayerData> onMongo = mongoStorage.repository(PLAYERS); // same descriptor💡 Tip — declare descriptors as
static finalconstants (often in oneDescriptorsholder class) and pass them around. Don't rebuild a descriptor per call; build once at startup.
- Quick Start — the descriptor in a full save/find/close flow.
- Entities, Keys & Collections — the deep dive on the key contract and collection regex.
-
Codecs —
Codec<V>SPI, JSON vs YAML,isJsonCodec(), custom codecs. -
Indexing & Queries —
@Indexedand manualIndexHinton the builder. -
Optimistic Locking —
@OptimisticLock,.versioned(),.version(getter, setter). - Choosing a Backend — which engines accept which codecs and capabilities.
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