navree
Documentation

MongoDB

The ORM's models on MongoDB collections: MongoModule, ObjectId keys and embedded documents, queries compiled to filters and pipelines, relations, units of work on sessions, migrations, time-series and TTL collections, and what works differently.

On this page

Featuremongo MongoDB is supported as a document database under the same model API: models declare driver = "mongodb", and every builder method compiles to a filter document or an aggregation pipeline instead of SQL. Records keep their dirty tracking, timestamps, soft deletes, scopes, events, relations and pagination.

Rust

Setting up

Turn on the mongo feature of the facade (also called mongodb; it is opt-in, not in full), add MongoModule, and point it at a server:

Cargo.toml
src/app.rs
config/app.toml

Every key is optional and can come from the environment (APP_DATABASE__MONGODB__URL). The URL's own options (?replicaSet=rs0&tls=true&authSource=admin) apply too; the keys win over them. TLS goes through rustls. Other keys set the pool (min_pool_size, max_idle_time_ms), timeouts (connect_timeout_ms, server_selection_timeout_ms), read_concern, journal, retry_writes, retry_reads and log_queries.

MongoModule provides MongoDb, the connection and an executor: inject Arc<MongoDb> and pass it where SQL code passes &Db. It also registers a mongodb readiness check for /health/ready and the mongo:migrate commands. navree::mongo re-exports the official driver (navree::mongo::mongodb) and BSON (navree::mongo::bson, with doc!) for what the module doesn't wrap.

Models

A model names its collection (collection = "deals", or table, which means the same) and the driver. Its key lives in the document's _id: a field id is stored there.

Key typeValue
ObjectId (navree::mongo::ObjectId)Generated before the insert when it is all zeros, so the record knows its key without a round trip; ObjectIds sort by creation time.
i64 and the other integersCounted per collection in a navree_sequences collection, one atomic counter each.
Uuid, Ulid, StringAs on SQL databases: generated, or set by you.

Embedded documents and arrays are JSON casts: #[model(cast = "json")] on a struct, a Vec, a map. Conditions reach inside them with paths, as on SQL databases: col("terms->currency").eq("EUR") is {"terms.currency": "EUR"}. Date-times are kept to the millisecond: the ORM truncates timestamps before saving, so a saved record equals its stored document.

Queries

A query compiles to a find when its stages fit one (a filter, an order, a page, a projection), and to an aggregation pipeline otherwise. to_mongo() shows the command without running it:

Rust
Text
QueryMongoDB
comparisons with a valuethe query language: {"amount": {"$gte": 10000}}
comparisons between fields or computed values$expr
LIKEan escaped $regex, anchored unless the pattern starts or ends with %
contains_ignore_case, eq_ignore_casethe text as an escaped $regex with the i option, which folds non-ASCII letters too and keeps accents
date parts$dateToString, $dayOfMonth, $month, $year, in UTC
JSON paths, containment, length, keysdot paths, $all, $size, $exists
full text$text
joins$lookup and $unwind
EXISTS, IN (subquery), scalar subqueriesa correlated $lookup
unions$unionWith
groups and HAVING, DISTINCT$group, then $match
time_bucket, first, lastTimescaleDB's alignment in milliseconds, $dateTrunc for months and years; $top, $bottom
a plain countcountDocuments

Aggregates, plucks, pagination (offset and cursor), chunks, lazy streams and cursors work as on SQL databases; streams read MongoDB cursors batch by batch.

MongoDB's own language

For what typed columns don't say, write a filter document, or add pipeline stages after the query's own:

Rust

Filter::document joins the query's other conditions like any condition; SQL databases refuse it. MongoStream::stream(exec) yields a query's records as the cursor delivers them.

Relations

Relations work on MongoDB: belongs-to, has-one, has-many and polymorphic eager loads are batched $in queries, many-to-many relations join their pivot collection with $lookup (and attach, sync and friends write pivot documents), and existence checks and relation counts (where_has, with_count) compile to $lookups.

Rust

A limit per parent in a constrained eager load (with_constrained(.., |q| q.limit(3))) needs a window function, which the MongoDB compiler refuses with OrmError::Unsupported: load the relation without the limit, or query each parent's records.

Writes and units of work

Inserts are one insertMany, updates one updateMany ($set, $inc, or an update pipeline for computed values), deletes one deleteMany. A duplicate key is a unique violation, a 409 over HTTP. The bulk writes work too: insert_or_ignore skips duplicates, upsert updates by the unique fields.

A unit of work on MongoDB is a session with a transaction, carrying the actor and request id, the change events of model writes and after-commit hooks:

Rust

mongo.transaction(|uow| async move { .. }) runs a closure in a unit of work and runs it again, up to three times, on a transient transaction error such as a write conflict:

Rust

On commit, the unit of work shows its change events to the app's MongoChangeObservers (registered with m.mongo_change_observer::<O>()) inside the transaction, commits, then runs the after-commit hooks; model listeners hear "-ed" events then. scope makes it the task's current unit of work, which a plain MongoDb of the same connection joins, and mongo.session() gives a session without a transaction (reads see their writes).

mongo.begin_in(&uow) joins a SQL unit of work, the request's: change events go to it (and to the audit log), and the MongoDB transaction commits in an after-commit hook of it. The two commits are not atomic together; a MongoDB commit that fails after the SQL one is logged.

Transactions need a replica set or a sharded cluster. A single-node replica set is enough for development (mongod --replSet rs0, then rs.initiate()); a standalone server's refusal says so.

Migrations

An app on MongoDB alone needs no SQL database: MongoDB has its own migrations, written with navree-schema's blueprints:

src/modules/deals/mongo_migrations/m20261027_090000_create_deals.rs

A module lists them in a MongoMigrationSet and contributes it:

src/modules/deals/mongo_migrations/mod.rs

create makes the collection with a $jsonSchema validator built from the columns (types, null for nullable ones, string lengths, enum values), then its indexes:

Text

table adds, changes, renames and drops fields in every document, with their indexes, and updates the validator. A unique index over a nullable field is partial, so NULLs don't collide; MongoDB enforces no foreign keys, so a foreign key becomes an index, with a notice. Migrations are recorded in navree_migrations, and the commands mirror the SQL ones:

Shell

Schema changes run outside transactions (MongoDB refuses most in one); a data migration returns true from within_transaction, and its writes through schema.uow() commit with its record. migrate_on_boot runs pending migrations while MongoDb is built.

Time series and expiring documents

A hypertable is a time-series collection on MongoDB: the time column becomes the timeField, a space partition the metaField, the chunk interval the granularity, and the retention expireAfterSeconds. So the same migration makes a TimescaleDB hypertable and a MongoDB time-series collection:

Rust

MongoDB takes no validator on time-series collections, and can't turn an existing collection into one (the error gives the steps). Described continuous aggregates become views, and capped(size, max), validation_level and validation_action set MongoDB's collection options. MongoSchema::pretending() records the commands instead of running them.

What works differently

  • Transactions need a replica set; schema changes run outside them.
  • Refused, with an OrmError::Unsupported that names what to do instead: raw SQL (conditions, expressions, orders), row locks (two transactions writing the same document conflict, and one is retried), RIGHT JOIN, and full text inside OR, NOT or a group.
  • NULL. Comparisons follow SQL where it matters: no ordering comparison holds for NULL or a missing field, and NULL keys join nothing. Negations (<>, NOT IN, NOT LIKE, where_not) keep MongoDB's rule: they match documents whose field is null or missing.
  • Date-times are kept to the millisecond.
  • Commands are reported to telemetry with their shape (values replaced by ?), so Logbook and slow-command warnings work as for SQL.

Testing

A test runs against a real server from NAVREE_TEST_MONGODB_URL (a replica set), each in a database of its own; without the variable, it returns early with a notice, as the SQL server tests do.