-
Notifications
You must be signed in to change notification settings - Fork 1
MongoDB
What this page covers: opening the MongoDB backend with Storages.createMongo, the two
MongoConfig constructors (connection string + database, optionally a connect timeout), how
entities are stored as native BSON sub-documents (not escaped JSON strings), how secondary
indexes become real Mongo indexes, and the one thing to know up front — transactions need a
replica set. Everything else (CRUD, queries, optimistic locking) is identical to every other
backend.
📌 Note — switching to Mongo is a one-line change at construction. From
storage.repository(...)onward your code is byte-for-byte identical to the SQL or in-memory path. 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.mongo.MongoConfig;
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 the Mongo collection name
.keyExtractor(PlayerData::getUuid)
.codec(new JacksonJsonCodec<>(PlayerData.class)) // Mongo requires a JSON codec
.build();
// 2. Open the backend.
MongoStorage storage = Storages.createMongo(
new MongoConfig("mongodb://localhost:27017", "mydb"));
storage.init().join(); // pings the server to verify the connection
// 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(); // upsert
Optional<PlayerData> alice = repo.find(id).join(); // -> Optional[Alice]
storage.close().join();createMongo returns the concrete MongoStorage type, so its capability interfaces
(TransactionalStorage, SchemaAwareStorage) are reachable without a cast. 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; in real code compose withthenApply/thenCompose. There are no blocking variants — see The Async API.
Two constructors, both taking a standard MongoDB connection string (URI format) and a database name:
import java.time.Duration;
import java.util.Optional;
// Minimal — uses the driver's default connect timeout.
new MongoConfig("mongodb://localhost:27017", "mydb");
// With auth and an explicit socket connect timeout.
new MongoConfig(
"mongodb://user:pass@host:27017",
"mydb",
Optional.of(Duration.ofSeconds(10))); // empty = driver default| Argument | Type | Meaning |
|---|---|---|
connectionString |
String |
MongoDB URI (mongodb://… / mongodb+srv://…), incl. credentials, hosts, options |
database |
String |
the database name to use |
connectTimeout |
Optional<Duration> |
socket connect timeout; empty leaves the driver default in place |
The connection string is the canonical place for everything Mongo-specific — auth source, TLS, read preference, replica-set name. The third argument only overrides the socket connect timeout; put the rest in the URI.
📌 Note —
init()issues apingcommand to verify the connection before completing. A bad URI or unreachable server makesinit()'s future complete exceptionally (the cause is aRuntimeExceptionwrapping the driver error). Always.join()/ handleinit()before opening repositories.
Each entity becomes one document in the collection named by your descriptor. The entity payload is
stored as a native BSON sub-document — a real nested document, not an escaped JSON string —
under storage_data:
// one document in the "players" collection
{
"_id": "5f1e8400-e29b-41d4-a716-446655440000", // key.toString() — the entity key IS the document _id
"storage_data": { "uuid": "5f1e…", "name": "Alice", "score": 100 }, // real sub-document
"_idx_score": 100, // present per declared IndexHint, populated at save time
"lock_version": 0 // present only for versioned descriptors
}📌 Note — the entity key is stored as the document
_id(not a separatestorage_keyfield), so identity is enforced by Mongo's built-in unique_idindex for free. It also lets Cross-Process Cache Sync recover the key from a Change-Stream delete event (documentKey._id) with no pre-images.
Because storage_data is a genuine sub-document, you can read and query it with any standard Mongo
tooling (Compass, mongosh, aggregation pipelines) — it isn't a blob you have to unescape.
⚠️ Gotcha — Mongo requires a JSON codec.storage.repository(...)throwsIllegalArgumentExceptionif the descriptor's codec isn't a JSON codec (codec.isJsonCodec()), because the payload is stored and parsed as native JSON/BSON.JacksonYamlCodecis only accepted by Local Files. UseJacksonJsonCodechere. See Codecs.
The key is persisted as key.toString() and must be a stable, unique string of ≤ 255
characters (the value's equals/hashCode must be consistent). An oversized key makes save's
future complete exceptionally with IllegalArgumentException — see
Entities, Keys & Collections. Collection names must match ^[a-zA-Z][a-zA-Z0-9_]*$.
Secondary indexes are declared, not implicit — via @Indexed on entity fields or manual
IndexHints on the descriptor (see Indexing & Queries). On Mongo, each declared hint becomes a
sibling _idx_<field> field populated at save time, with a real Mongo index created over it:
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.index(IndexHint.integer("score")) // -> _idx_score, real ascending index
.codec(new JacksonJsonCodec<>(PlayerData.class))
.build();
// queries hit the index, not a scan:
repo.findBy("score", 100).join();
repo.query(Query.range("score", 50, null)).join(); // score >= 50 (inclusive, null = open end)Entity identity rides on the document _id (the key), which Mongo already indexes uniquely — so
there's no separate identity index to create. The first time a repository is opened, Mongo reconciles
the _idx_* indexes with the descriptor: new hints are created and backfilled onto existing
documents; removed hints get their _idx_* index dropped. So adding or dropping an index is just
editing the descriptor and reopening — no manual createIndex.
📌 Note — querying a field that was not declared as an index throws
IllegalArgumentException, the same as on every backend. Declare it first.
TIMESTAMP index fields are compared as epoch-millis everywhere. See Indexing & Queries for the
full IndexHint factory table and Query semantics.
MongoStorage implements tx.TransactionalStorage, so you can run a unit of work atomically —
but multi-document transactions require a MongoDB replica set (MongoDB 4.0+).
// Repositories obtained from the scope share one session; commit on success,
// rollback on exception or scope.rollback().
storage.inTransaction(scope -> {
Repository<UUID, PlayerData> txRepo = scope.repository(PLAYERS);
return txRepo.save(alice)
.thenCompose(__ -> txRepo.save(bob));
}).join();
⚠️ Gotcha — on a standalone mongod,inTransaction(...)throws at runtime (the driver rejects starting a session transaction). Run a replica set — even a single-node one (rs.initiate()) is enough to enable transactions. The library can't paper over this: it's a server-topology requirement, not a flag.
A first-insert race inside a transaction on a versioned descriptor surfaces as
OptimisticLockException (the duplicate-key error is converted, never leaked as a raw
MongoWriteException). See Transactions and Optimistic Locking.
Opt in per descriptor (annotate a long/Long field with @OptimisticLock, or
.versioned() / .version(getter, setter) — see Optimistic Locking). Mongo enforces it: a
lock_version field is stored, starts at 0, and an update only succeeds if the stored version
matches — otherwise the future completes exceptionally with OptimisticLockException (surfaced as a
CompletionException cause through .join()).
📌 Note — Mongo is one of the three backends that enforce optimistic locking (with MySQL/MariaDB and PostgreSQL). H2 silently degrades to plain upsert; local files and in-memory don't enforce it. See the matrix in Choosing a Backend.
MongoStorage implements schema.SchemaAwareStorage. Applied versions are tracked in a reserved
_schema_migrations collection; migrations are forward-only. Extend MongoMigration and
override executeOnDatabase(MongoDatabase):
import br.com.finalcraft.everydatabase.modules.mongo.MongoMigration;
import com.mongodb.client.MongoDatabase;
class V1_SeedFlags extends MongoMigration {
@Override public String version() { return "001"; }
@Override public String description() { return "seed feature flags"; }
@Override protected void executeOnDatabase(MongoDatabase db) {
db.getCollection("flags").insertOne(/* ... */);
}
}
storage.register(new V1_SeedFlags()).migrate().join();MigrationContext.getNativeClient(MongoDatabase.class) also exposes the raw database inside a
migration. See Schema Migrations.
MongoStorage implements changefeed.ChangeFeedStorage, the best push transport the library has:
MongoDB Change Streams are resumable, so an instance that briefly reconnects misses nothing within
the oplog window. Both SAVE and DELETE propagate — because the entity key is the document _id, a
delete event carries the key with no pre-images. Wire it through the manager's CacheSync so other
instances invalidate their caches when one writes:
CacheSync.attach(mongoStorage)
.bind(guildsManager) // -> push (change stream); no pollEvery needed
.start();
⚠️ Gotcha — Change Streams require a replica set (the same requirement as transactions). On a standalonemongodthe feed isn't available; the manager falls back to polling only if you set.pollEvery(...). The docker-compose Mongo is a 1-node replica set for exactly this reason.
See Cross-Process Cache Sync for the full picture (push vs poll, key parsing, the own-origin skip).
🧭 Decision — choose MongoDB when your data is document-shaped, you already run Mongo, or you want native BSON sub-documents queryable in Mongo tooling. It gives the same guarantees as the SQL backends — full transactions, enforced optimistic locking, real indexes — with the single caveat that transactions need a replica set. If you don't need documents-on-a-server, a SQL backend (MySQL & MariaDB / PostgreSQL) or H2 may be simpler to operate.
You can migrate live data into or out of Mongo with no code changes — see Moving Data Between Backends.
- Choosing a Backend — the capability matrix and data-at-rest formats across all backends.
- Quick Start — the minimal describe → open → use → close round-trip.
-
The Async API — the
CompletableFuturemodel every call shares. -
Transactions —
inTransaction, scope-bound repositories, the replica-set requirement. -
Indexing & Queries —
@Indexed/IndexHint, theQueryAPI. -
Optimistic Locking —
@OptimisticLock,OptimisticLockException, the enforce-vs-degrade matrix. -
Schema Migrations —
MongoMigration,_schema_migrations, forward-only. - Cross-Process Cache Sync — Change Streams as the push feed for cache invalidation.
- Codecs — why Mongo requires a JSON codec.
- Entities, Keys & Collections — the key/collection contract.
- Moving Data Between Backends — copy live data to/from Mongo.
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