Skip to content

Saving aggregates

A view also writes. The same declaration that loads an aggregate saves it back, on any of the three databases, in a transaction — or a savepoint of yours (MPA-WRITE-1).

mabat::save(&mut board, &mut tx).await?; // the board and what it owns
mabat::save_changes(&before, &mut after, &mut tx).await?; // only what changed
mabat::delete::<Board, _>(board.id, &mut tx).await?; // the board and what it owns

Statements run when they are called; there is no session to flush. Keys come from the application, so a saved view and the views of its owned collections need a key field (MPA-WRITE-2).

PartWritten asRule
Columns, embedded values, JSONthe row, updated by key, or inserted if newMPA-WRITE-3
An owned collectionmade equal to the value’s: gone elements deleted with what they own, the rest saved with their position or map keyMPA-WRITE-4
Many-to-manythe link rows, replacedMPA-WRITE-5
A to-one referencethe foreign key onlyMPA-WRITE-5
An enumthe tag and variant columns, or the variant’s table rowMPA-WRITE-6
let before = mabat::load::<Board>().by_key(id).one(&mut tx).await?;
let mut after = before.clone();
after.name = "Roadmap 2".into();
mabat::save_changes(&before, &mut after, &mut tx).await?; // UPDATE "board" SET "name" = $1 WHERE "id" = $2

Fields are compared with PartialEq and elements matched by key: changed rows get UPDATE … SET <changed>, new elements are saved, removed ones deleted, and unchanged rows produce no statement (MPA-WRITE-8). It also updates views of some of a table’s columns, which a whole save cannot insert (MPA-WRITE-12).

There is no session watching your values, so nothing is tracked behind your back: before is the snapshot, and the comparison runs when you call save_changes. Change tracking, against an ORM sets this beside the proxies, snapshots and flushes of an ORM’s session.

#[derive(View, Clone, PartialEq)]
#[view(table = "doc")]
struct Doc {
id: i64,
title: String,
#[view(version)]
version: i32,
}

A row with a version column is written only if it still has the value’s version, which the write increments; otherwise the write fails with Error::Conflict and rolls back (MPA-WRITE-9). The new versions are written back into the value and its owned elements, so it can be saved again without reloading (MPA-WRITE-10).

mabat::save_all saves many values in one transaction, each as save saves one, but with statements for each table and level of the aggregate instead of for each row (MPA-WRITE-19):

mabat::save_all(&mut tasks, &mut tx).await?;

For the rows of each table, one statement updates those that exist from a table of their values, and one inserts the others; then the rows of their collections, variant tables and links follow the same way. Statements are split to stay under the databases’ limits on parameters.

Saving 1,000 tasks of 10 subtasks (local PostgreSQL)New rowsRows that exist
save, one task at a time1.0 s1.6 s
save_all0.12 s0.08 s

Over a network, the gap grows with the round-trip time: one task at a time takes thousands of round trips, and save_all a few dozen. Run cargo run --release -p mabat --example save_all to measure your own. Rows whose keys the database generates are inserted one at a time, to read their keys; their collections are still batched.

Mark the key field #[view(generated)] and make it an Option of an integer. save inserts a value whose key is None without its key column, reads the key the database generated, writes what the value owns under it, and writes the key back into the value and into its new elements (MPA-WRITE-13):

#[derive(View)]
#[view(table = "project")]
struct Project {
#[view(generated)]
id: Option<i64>,
name: String,
#[view(child(fk = "project_id", index = "position"))]
tasks: Vec<Task>,
}
let mut project = Project { id: None, name: "Alpha".into(), tasks: vec![task("Design")] };
mabat::save(&mut project, &mut tx).await?; // INSERT INTO "project" ("name") VALUES ($1) RETURNING "id"
let id = project.id.unwrap(); // and project.tasks[0].id, if Task's key is generated too
DatabaseThe columnHow the key is read
PostgreSQLBIGINT GENERATED BY DEFAULT AS IDENTITY, or bigserialRETURNING
MySQLBIGINT AUTO_INCREMENTLAST_INSERT_ID()
SQLiteINTEGER PRIMARY KEYRETURNING

A value that has a key is saved like any other, so the column must accept keys given explicitly: on PostgreSQL use GENERATED BY DEFAULT, not GENERATED ALWAYS. With save_changes, elements without a key are new and are inserted the same way. A value that was never saved has no changes to save: save_changes refuses it, and so does a reference or a link to it, with Error::Write.

Views with Ref<T> fields are saved as a whole graph with save_graph; see Saving a graph.