navree
Documentation

Getting started

Navree's ORM: models as plain structs with #[derive(Model)], typed columns, a query builder, relationships and records that track their changes, running in the request's unit of work.

On this page

The ORM is how a Navree app works with its data. A model is a plain Rust struct with #[derive(Model)]; the derive maps it to a table and gives it one typed column constant per field, so conditions are checked by the compiler. Queries chain on a builder and run on the executor you pass them: the request's unit of work in a handler, the database pool elsewhere. What a query returns are records: your model plus what the ORM knows about it, such as whether it is stored and which fields you changed.

Rust

Around models and queries the ORM has relationships with batched eager loading, casts and accessors, serialization that hides what clients must not see, events and observers, scopes and soft deletes, pagination and streams, factories and testing helpers, route model binding, and workspace and access-control scoping.

It runs on what navree-db already owns: the connection pool, the unit of work and its transaction, change events for the audit log, after-commit hooks, and the actor and workspace settings that triggers and row-level security policies read. A record saved in a handler is part of the request's transaction: it commits when the response succeeds, rolls back when it fails, and shows up in the audit trail.

The same models run on every database Navree supports: TimescaleDB and PostgreSQL, MySQL and MariaDB, SQLite, and MongoDB through a document driver. SQL is generated per database, with every value bound.

Installation

Featureorm The ORM is the orm feature of the navree facade. full, the default, turns it on, and it turns on the features it builds on: db, http, events, telemetry, crypto and api. Everything lives in navree::orm; its prelude has every name a model or a query needs:

Rust

navree::prelude brings the everyday ones too: Model (the trait and the derive), Record, Collection, Builder, OrmModule, Attributes, attrs! and #[accessors].

Models need serde's Serialize, so your app depends on serde with its derive feature, as an app made by navree new does. The ORM needs no other crate: SQL is generated with sea-query and runs through SeaORM's connections, both of which come with navree.

Configuration

Models work without any module, but an app adds OrmModule to configure them. navree new puts it in src/app.rs:

src/app.rs

OrmModule reads the orm section of your configuration. Every key is optional:

config/app.toml
KeyDefaultWhat it does
stricton, except in productionTurns the three prevent_* modes on or off together. Unset, it is on unless the app's environment (env, APP_ENV, or app.env) is production: development and tests are strict, production is lenient.
prevent_missing_attributesstrictReading a record whose query didn't select one of the model's columns fails with OrmError::MissingAttribute, instead of filling the field with its type's default.
prevent_discarding_attributesstrictMass assignment of a key the model doesn't allow fails with OrmError::MassAssignment, instead of leaving the key out.
prevent_lazy_loadingstrictLoading a relation for one record of a result fails with OrmError::LazyLoading, so N+1 queries show up in development. See preventing lazy loading.
auto_eager_loadfalseA relation loaded for one record of a result is loaded for every record of that result, in one query.
prune_chunk1000How many records model:prune handles at a time.

A mode you set on its own wins over strict. Like every section, these keys can come from the environment: APP_ORM__PREVENT_LAZY_LOADING=false.

To configure the module in code, pass OrmOptions to OrmModule::for_root, and to log a violation instead of failing, give it a handler:

Rust

on_missing_attribute and on_discarded_attributes do the same for the other two modes. The module also registers the commands model:prune and model:show, and the job that queues model broadcasts.

Your first model

Generate a model in a module, with the migration that creates its table:

Shell

This writes src/modules/deals/models/deal.rs, a create_deals_table migration, and registers the model in the module's wire (m.model::<Deal>()). --factory, --seeder, --controller and --policy add the companions, which refer to each other, and --all adds every one of them.

A model is a struct with named fields:

src/modules/deals/models/deal.rs

The derive reads everything from the struct:

  • id is the primary key. Its type decides how it gets a value: a Uuid key is a UUID v7, generated when the record is created, so keys sort by creation time and the record knows its key before the INSERT.
  • timestamps makes the ORM set created_at when the record is inserted and updated_at whenever it changes, from the app's dyn Clock.
  • Each field is a column of the same name, read and written as its Rust type.
  • table names the table. Without it, the table is the struct's name in snake case and plural, so Deal maps to deals and Company to companies.

The migration creates the table with the schema builder:

src/modules/deals/migrations/m20261027_090000_create_deals_table.rs

Models covers keys of other types, column names that differ from field names, soft deletes and every other option.

Running queries

Every method that touches the database is async and takes an executor first: &Db (the connection pool) or &Uow (a unit of work).

Rust

Deal::create returns a Record<Deal>, and get a Collection<Deal> of records. A record dereferences to the model, so deal.title reads the field and deal.amount_cents = .. changes it. Deal::AMOUNT_CENTS is a Column<Deal, i64>: comparing it with a string would not compile.

ExecutorReads run onWrites run on
&Uowits transactionits transaction, with change events recorded for the audit log
&Dbthe current unit of work's transaction when it is open, else the pool (a read replica when you configure them)the current unit of work when there is one, else the pool
a SeaORM DatabaseConnection or DatabaseTransactionas givenas given, without change events

In a handler, pass the request's unit of work. &Db joins it as well (the current unit of work is the request's while a handler runs), so a service that only has an Arc<Db> still writes inside the request's transaction. In commands, jobs and seeders, &Db runs on the pool, or open a unit of work with db.transaction(..). Connections and transactions has the details: named connections, read replicas and raw SQL.

Models in a controller

Here is a JSON API for deals. Bound<Deal> loads the deal the path names, or answers 404; paginate reads a page and the total in two queries:

src/modules/deals/controller.rs

Records serialize as their model's JSON, so a handler returns them as they are; serialization shows how to hide fields and add computed ones. navree make:model deals Deal --controller writes a controller like this one for you, and --policy one whose routes check abilities.

Services take the unit of work as a parameter, like handlers:

src/modules/deals/service.rs

Errors

ORM methods return navree::orm::Result<T>, whose error is OrmError. A handler returns it with ?, and it keeps its meaning over HTTP:

ErrorResponse
ModelNotFound (find_or_fail, first_or_fail, sole, a route binding)404, "Not Found"
A unique or foreign-key violation409, as for every database error (database errors)
Anything else500, logged with its details

Messages name the model, the column and the operation, and say how to fix the problem: "cannot read column amount_cents of Deal as i64: ...", "no Deal found for key 0199a7a0-...". No SQL or value reaches the client. is_not_found(), is_unique_violation(), is_timeout() and is_lost_connection() classify an error when you handle it yourself.

Inspecting models

navree model:show lists the models the app's modules register, and navree model:show Deal describes one (--json prints it as JSON):

Text

Below the columns it lists the factory's relationships, the global scopes and the hooks, when the model has them. A module registers a model with m.model::<Deal>() (ModuleBuilderOrmExt, in the prelude); make:model adds the line.

The ORM and SeaORM entities

The ORM and SeaORM share the pool, the unit of work and the values they bind, so SeaORM entities and Repo keep working next to models, in the same transaction. Several of Navree's own modules still use SeaORM entities. For new code, use models.