Ormer
Home
Quick Start
GitHub
  • 简体中文
  • English
Home
Quick Start
GitHub
  • 简体中文
  • English
  • Introduction to Ormer
  • Quick Start
  • Model Definition
  • Database Connection
  • Data Operations
  • Query Builder
  • Advanced Queries
  • Transaction Management
  • Connection Pool
  • Hooks System
  • Migrations

Hooks System

Hooks provide fallible lifecycle callbacks for write operations. Every callback receives a HookContext and returns ormer::Result<()>; validation should return an error instead of panicking.

Hook TraitTimingSignature
BeforeInsertBefore insert SQLasync fn before_insert(&mut self, ctx: &mut HookContext<'_>) -> Result<()>
AfterInsertAfter successful insert SQLasync fn after_insert(&self, ctx: &mut HookContext<'_>) -> Result<()>
BeforeUpdateBefore update SQLasync fn before_update(&mut self, ctx: &mut HookContext<'_>) -> Result<()>
AfterUpdateAfter an update affects at least one rowasync fn after_update(&self, ctx: &mut HookContext<'_>) -> Result<()>
BeforeDeleteBefore delete SQLasync fn before_delete(&self, ctx: &mut HookContext<'_>) -> Result<()>
AfterDeleteAfter a delete affects at least one rowasync fn after_delete(&self, ctx: &mut HookContext<'_>) -> Result<()>

Inserts

Insert hooks use mutable model inputs so BeforeInsert can normalize or fill fields. For models implementing BeforeInsert and AfterInsert, insert, insert_or_update, insert_or_ignore, and their transaction executors run hooks in BeforeInsert -> SQL -> AfterInsert order.

use ormer::{
    AfterInsert, BeforeInsert, Database, DbType, HookContext, Model,
};

#[derive(Debug, Model)]
#[table = "users"]
struct User {
    #[primary(auto)]
    id: i32,
    email: String,
}

#[async_trait::async_trait]
impl BeforeInsert for User {
    async fn before_insert(&mut self, _ctx: &mut HookContext<'_>) -> ormer::Result<()> {
        self.email = self.email.trim().to_lowercase();
        if !self.email.contains('@') {
            return Err(ormer::ormer_error!("invalid email"));
        }
        Ok(())
    }
}

#[async_trait::async_trait]
impl AfterInsert for User {
    async fn after_insert(&self, _ctx: &mut HookContext<'_>) -> ormer::Result<()> {
        Ok(())
    }
}

let db = Database::connect(DbType::Sqlite, "app.db").await?;
db.create_table::<User>().execute().await?;

let mut user = User {
    id: 0,
    email: "  Alice@example.com ".into(),
};
user.id = db.insert(&mut user).execute().await?;

Pass a mutable collection for a batch insert. Hooks run once per record and ctx.batch_index() identifies that record.

let mut users = vec![user_a, user_b];
db.insert(&mut users).execute().await?;

An error from BeforeInsert prevents SQL execution. An error from AfterInsert is returned after SQL has run; for transaction operations, the caller should call rollback() when that is the required business outcome.

Updates and Deletes

An ordinary update or delete execute() runs SQL without a model hook subject. Use execute_with_hooks to supply the model. Use execute_models_with_hooks for batches; each context carries its batch index.

db.update::<User>()
    .set_model(&user)
    .execute_with_hooks(&mut user)
    .await?;

db.delete::<User>()
    .filter(|fields| fields.id.eq(user.id))
    .execute_with_hooks(&user)
    .await?;

For an insert inside a transaction, ctx.in_transaction() is true. Hook failures are returned as Result and do not commit the transaction; the caller chooses commit() or rollback().

Write hooks are enabled by default. Use without_hooks() only for the current execution chain; it does not affect other tasks or connections.

The current release does not provide a cross-connection database change listener. Raw SQL and writes from another connection or process do not construct models or trigger WriteHook.

SQL Trace

sql_trace() registers global SQL execution callbacks. Use it to record SQL, parameter views, elapsed time, errors, slow SQL, and to rewrite SQL text before execution.

let db = ormer::Database::connect(ormer::DbType::Sqlite, ":memory:").await?;

db.sql_trace()
    .before(|sql| println!("before: {sql}"))
    .after(|sql, elapsed| println!("after: {sql} {elapsed:?}"))
    .on_error(|sql, err| eprintln!("error: {sql} {err}"))
    .slow_sql_threshold(std::time::Duration::from_millis(100))
    .slow(|sql, elapsed| eprintln!("slow: {sql} {elapsed:?}"));

Use before_with, after_with, or on_error_with when callbacks need parameters. Parameter redaction only changes the callback view; it does not change the bound values sent to the database.

db.sql_trace()
    .redact_params(|params| {
        params.iter().map(|_| ormer::Value::Text("***".into())).collect()
    })
    .before_with(|event| {
        println!("sql={} params={:?}", event.sql(), event.params());
    });

HookContext

HookContext exposes:

  • operation() for Insert, Update, or Delete.
  • batch_index() for the index in a batch, or None for a single record.
  • in_transaction() to indicate execution through Transaction.
Last Updated: 8/26/26, 11:48 PM
Contributors: fawdlstty
Prev
Connection Pool
Next
Migrations