Model Hooks

Hooks let you run custom logic before or after single-row database operations.

Implementing hooks

Hooks are opt-in. #[derive(Model)] emits an empty impl ModelHooks for you; add #[model(hooks)] to write your own instead.

#![allow(unused)]
fn main() {
use rango::prelude::*;

#[derive(Model)]
#[model(hooks)]
pub struct User {
    pub id: FieldUuid,
    pub email: FieldEmail,
    pub slug: FieldVarchar<0, 100>,
}

impl ModelHooks for User {
    fn before_save(&mut self) -> BoxFuture<'_> {
        Box::pin(async move {
            // Runs before every INSERT or UPDATE; the mutation is what gets written.
            self.slug = FieldVarchar::new(self.email.replace('@', "-at-"))?;
            Ok(())
        })
    }

    fn after_create(&self) -> BoxFuture<'_> {
        Box::pin(async move {
            println!("Welcome {}!", self.email);
            Ok(())
        })
    }
}
}

#[rango::model(...)] forwards the attribute: write #[model(hooks)] on the struct as well.

Available hooks

HookTriggered
before_saveBefore INSERT or UPDATE
after_saveAfter INSERT or UPDATE
before_createBefore INSERT only
after_createAfter INSERT only
before_updateBefore UPDATE only
after_updateAfter UPDATE only
before_deleteBefore DELETE
after_deleteAfter DELETE

Which operations fire hooks

OperationOrder
insertbefore_save, before_create, SQL, after_create, after_save
updatebefore_save, before_update, SQL, after_update, after_save
deletebefore_delete, SQL, after_delete
get_or_createcreate hooks on defaults: before_* always run before the statement, after_* only if a row was inserted
update_or_createcreate hooks or update hooks, depending on whether the row existed

Bulk operations skip hooks. bulk_create, bulk_update and bulk_upsert never call any hook (see Bulk operations). If a hook computes a field, such as the slug above, compute it yourself before a bulk call or insert row by row.

Not fired (same as Django): query-builder update / delete and raw*, as well as the bulk operations above.

before_* hooks take &mut self, so they can change the model; after_* hooks see the row returned by the database.

Errors

  • All hooks are async and return Result<(), Box<dyn Error + Send + Sync>>.
  • A before_* hook returning Err cancels the operation: no SQL runs. The caller gets Error::Hook.
  • An after_* hook returning Err is returned to the caller as Error::Hook too, but the statement has already run. Inside atomic() (or any transaction) the error rolls the write back. With a plain pool the write is already committed: use atomic() if an after_* hook must be able to undo it.
  • Hooks never open a transaction by themselves.
#![allow(unused)]
fn main() {
impl ModelHooks for User {
    fn before_save(&mut self) -> BoxFuture<'_> {
        Box::pin(async move {
            if self.email.is_empty() {
                return Err("email cannot be empty".into());
            }
            Ok(())
        })
    }
}
}