Registration (Upsert)
Find or insert a row on an alternate unique key, and say what it did.
A source system that re-sends its own content every cycle needs the write to be
idempotent, and needs the answer keyed by what it holds rather than by position
in the request. crudcrate::upsert does both in one transaction.
Declaring the key
Name the columns the source identifies its rows by:
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, EntityToModels)]
#[sea_orm(table_name = "curves")]
#[crudcrate(api_struct = "Curve", upsert_key(source_system, source_key))]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
#[crudcrate(primary_key, exclude(create, update), on_create = Uuid::new_v4())]
pub id: Uuid,
pub source_system: String,
pub source_key: String,
pub slope: f64,
}
The key is an alternate identity, not the primary key. The row keeps its own
id, which is what the rest of your API refers to.
A key that is unique only sometimes
Where the unique index backing the key is partial, name its predicate too. The find is narrowed by it, so a row the index does not cover is not the key’s row:
#[crudcrate(
api_struct = "Episode",
upsert_key(site_id, parameter_id),
upsert_where = Column::ResolvedAt.is_null()
)]
against
CREATE UNIQUE INDEX episodes_open_uniq
ON episodes (site_id, parameter_id) WHERE resolved_at IS NULL;
An open episode registers against itself. Once it is resolved it leaves the index, so the next registration of that slot opens a second episode rather than reopening the closed one, and the history of the first survives.
What you write is a SeaORM expression, the same value OnConflict::target_cond_where
takes, so the predicate is the index’s own WHERE clause rather than a syntax
of this crate’s. Naming it without an upsert_key is a compile error: there is
no find for it to narrow.
Registering a row
use crudcrate::{upsert, UpsertStatus};
let (curve, status) = upsert::<Curve, _>(&db, CurveCreate {
source_system: "cnet".to_string(),
source_key: "DOC:plate-7".to_string(),
slope: 1.5,
}.into()).await?;
match status {
UpsertStatus::Created => "the key was not stored here before",
UpsertStatus::Updated => "the key was stored and content differed",
UpsertStatus::Unchanged => "the key was stored and content already read as sent",
};
The key columns are read from the active model. For keys excluded from create
and update requests, convert the create model first, then set the key columns
from the authenticated caller before calling upsert. On Unchanged no write
happens.
What counts as content
The columns compared and written are the ones the create model can carry, minus the primary key. A field the entity maintains itself is not part of what a source sends, so it is neither compared nor overwritten:
created_atkeeps the value it was first stored with.- A field carrying
on_updateadvances when the row does change, and not on anUnchangedpass. - A field the active model leaves unset is not compared and not written.
Reporting a batch
UpsertOutcome reports one item keyed by what the sender holds:
pub struct UpsertOutcome<K, I, S = UpsertStatus> {
pub key: K,
pub id: Option<I>,
pub status: S,
pub note: Option<String>,
}
status is generic so a caller with refusals of its own (a row whose owner does
not exist here, a row it kept against what the source sent) reports them in the
same list rather than as failures. BatchResult, which is per-item
success-or-error, cannot express an item that was stored and still has something
to report.
Caveats
- Add a unique index on the key columns. Nothing generates one. Without it, two registrations racing on the same key can both find nothing and both insert.
- A resource declaring no
upsert_keyrefuses the call rather than guessing a key, as does an active model that leaves a key column unset. - A partial index needs
upsert_where. Without it the find matches rows the index does not cover, so a registration updates a row the index would have let it insert beside. upsertis a function you call, not a generated route. It is for an ingest path you write, not for the public API surface.
See also
- Default Values for
on_createandon_update - Struct Attributes