SaveChanges 拦截器
拦截器允许在 `save_changes()` 的多个阶段注入横切逻辑,典型场景包含**审计日志**、**软删除**、**验证**。
实现拦截器
use rust_ef::interceptor::{ISaveChangesInterceptor, SaveChangesContext, SaveChangesResultContext};
use rust_ef::error::EFResult;
struct AuditInterceptor;
#[async_trait::async_trait]
impl ISaveChangesInterceptor for AuditInterceptor {
async fn on_saving(&self, ctx: &SaveChangesContext) -> EFResult<()> {
tracing::info!(
"Saving +{} ~{} -{}",
ctx.added_count(),
ctx.modified_count(),
ctx.deleted_count()
);
Ok(())
}
async fn on_saved(&self, _ctx: &SaveChangesContext, result: &SaveChangesResultContext) -> EFResult<()> {
tracing::info!("Saved: {} entities modified", result.total());
Ok(())
}
async fn on_save_failed(&self, _ctx: &SaveChangesContext, err: &rust_ef::error::EFError) {
tracing::error!("Save failed: {}", err);
}
}
注册拦截器
let provider = ServiceCollection::new()
.add_dbcontext(|options| {
options
.use_sqlite("app.db")
.add_interceptor(AuditInterceptor);
})
.build()
.unwrap();
软删除:拦截器 + 手动标记
软删除(Soft Delete)不物理删除行,而是将 `is_deleted` 标记为 `true`,配合全局查询过滤器 自动隐藏已删除记录。由于当前 `SaveChangesContext` 只暴露**只读**的 `EntityEntryView`(含 `type_id` / `type_name` / `state`,不含实体引用),拦截器**无法**直接修改实体字段。因此 推荐采用"**手动标记 + 拦截器审计**"的分工模式:
| 角色 | 职责 | 实现方式 | |------|------|----------| | 应用代码 | 将 `is_deleted` 设为 `true` 并标记 Modified | `tracked_entries_mut()` + `detect_changes()` | | 拦截器 | 记录审计日志(谁、何时、改了什么) | `on_saving` / `on_saved` |
完整模式
use rust_ef::prelude::*;
use rust_ef::query::{BoolExpr, FilterCondition};
use rust_ef::provider::DbValue;
#[derive(Debug, Clone, EntityType)]
#[table("articles")]
struct Article {
#[primary_key]
#[auto_increment]
pub id: i32,
pub title: String,
/// true = 已软删除
pub is_deleted: bool,
}
// 1) 注册全局查询过滤器:只看到未删除记录
let soft_delete_filter = BoolExpr::Filter(FilterCondition::with_values(
"is_deleted", "=", vec![DbValue::Bool(false)],
));
ctx.model().has_query_filter::<Article>(soft_delete_filter);
ctx.set::<Article>();
ctx.ensure_created().await?;
// 2) 软删除:先把目标行载入 ChangeTracker,再改字段
ctx.load_all::<Article>().await?; // 查询 + attach 为 Unchanged
for entry in ctx.set::<Article>().tracked_entries_mut() {
if entry.title == "outdated" {
entry.is_deleted = true; // 修改字段
}
}
ctx.detect_changes(); // 快照比对 → 标记 Modified
ctx.save_changes().await?; // 生成 UPDATE,而非 DELETE
为什么不能用 `query().to_list()`?
`query().to_list()` 返回 `Vec<T>`,**不**把实体放入 ChangeTracker。`save_changes()` 之后 跟踪器会被清空,因此必须用 `load_all()`(查询 + `attach`)或手动 `attach()` 才能让 `tracked_entries_mut()` 看到条目。
管理员查询:绕过过滤器
// 普通查询:自动追加 AND is_deleted = false
let active = ctx.set::<Article>().query().to_list().await?;
// 管理员查询:查看全部记录(含已删除)
let all = ctx.set::<Article>().query_ignore_filters().to_list().await?;
审计拦截器
拦截器不适合做软删除本身(无法改实体),但非常适合记录"谁触发了保存"等审计信息:
struct AuditInterceptor;
#[async_trait::async_trait]
impl ISaveChangesInterceptor for AuditInterceptor {
async fn on_saving(&self, ctx: &SaveChangesContext) -> EFResult<()> {
tracing::info!(
"Saving +{} ~{} -{}",
ctx.added_count(), ctx.modified_count(), ctx.deleted_count()
);
Ok(())
}
async fn on_saved(
&self, _ctx: &SaveChangesContext, result: &SaveChangesResultContext,
) -> EFResult<()> {
tracing::info!("Saved: {} entities", result.total());
Ok(())
}
async fn on_save_failed(&self, _ctx: &SaveChangesContext, err: &rust_ef::error::EFError) {
tracing::error!("Save failed: {}", err);
}
}
完整可运行示例见 `examples/soft_delete`。
审计日志:缓冲区模式
拦截器无法直接访问数据库连接,因此不能在 `on_saving` 中写审计表。推荐采用 "**拦截器捕获 → 应用代码持久化**"的缓冲区模式:
1. 拦截器持有 `Arc<Mutex<Vec<AuditEvent>>>`,在 `on_saving` 中收集事件 2. `save_changes()` 返回后,应用代码 drain 缓冲区,写入 `audit_log` 表 3. 拦截器过滤掉 `AuditLog` 自身的事件,避免反馈循环
变更历史表设计
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL, -- 实体类型名(如 "Document")
action TEXT NOT NULL, -- INSERT / UPDATE / DELETE
occurred_at INTEGER NOT NULL, -- Unix epoch 秒
affected INTEGER NOT NULL -- 本次保存影响的行数
);
> 当前 `EntityEntryView` 只含 `type_name` + `state`,不支持字段级 diff。 > 如需记录变更前后的值,需扩展拦截器 API(未来迭代)。
实现
use std::sync::{Arc, Mutex};
use rust_ef::entity::EntityState;
#[derive(Clone)]
struct AuditEvent {
entity_type: String,
action: &'static str,
occurred_at: i64,
}
struct AuditInterceptor {
events: Arc<Mutex<Vec<AuditEvent>>>,
}
#[async_trait::async_trait]
impl ISaveChangesInterceptor for AuditInterceptor {
async fn on_saving(&self, ctx: &SaveChangesContext) -> EFResult<()> {
let now = now_epoch();
let mut buf = self.events.lock().unwrap();
for entry in ctx.entries() {
if entry.type_name == "AuditLog" { continue; } // 防反馈循环
let action = match entry.state {
EntityState::Added => "INSERT",
EntityState::Modified => "UPDATE",
EntityState::Deleted => "DELETE",
_ => continue,
};
buf.push(AuditEvent {
entity_type: entry.type_name.clone(),
action, occurred_at: now,
});
}
Ok(())
}
// on_saved / on_save_failed 省略
}
保存后持久化审计记录:
ctx.save_changes().await?;
// drain 缓冲区 → 写入 audit_log 表
let drained: Vec<AuditEvent> = audit_buffer.lock().unwrap().drain(..).collect();
for ev in &drained {
ctx.add::<AuditLog>(AuditLog {
id: 0,
entity_type: ev.entity_type.clone(),
action: ev.action.to_string(),
occurred_at: ev.occurred_at,
affected: 1,
});
}
ctx.save_changes().await?; // 注意:拦截器会跳过 AuditLog 事件
完整可运行示例见 `examples/audit`。
时间戳管理:CreatedAt / UpdatedAt
拦截器无法修改实体字段,因此 `created_at` / `updated_at` 需在应用代码中手动填充。
插入时
构造实体时直接设置两个时间戳:
let now = now_epoch();
ctx.add::<Document>(Document {
id: 0,
title: "Design Doc".into(),
body: "...".into(),
created_at: now, // 插入时设置
updated_at: now, // 插入时设置
});
ctx.save_changes().await?;
更新时
用 `load_all()` 载入跟踪器,修改字段后用谓词匹配戳记 `updated_at`:
ctx.load_all::<Document>().await?;
for doc in ctx.set::<Document>().tracked_entries_mut() {
if doc.title == "old title" {
doc.body = "new content".into();
}
}
// 只对已修改的条目戳记 updated_at,避免不必要的 UPDATE
stamp_updated_at(&mut ctx, |d| d.title == "old title");
ctx.save_changes().await?;
fn stamp_updated_at<F>(ctx: &mut DbContext, predicate: F)
where F: Fn(&Document) -> bool {
let now = now_epoch();
for doc in ctx.set::<Document>().tracked_entries_mut() {
if predicate(doc) { doc.updated_at = now; }
}
ctx.detect_changes();
}
> **注意**:`stamp_updated_at` 只戳记匹配谓词的条目。如果对所有跟踪条目统一戳记, > 会导致未修改的行也被标记为 Modified,产生多余的 UPDATE。
设计要点
| 实践 | 说明 | |------|------| | 拦截器按注册顺序执行 | 审计应在最前,验证在中间,软删除在最后 | | `on_saving` 可中止提交 | 返回 `Err` 会阻止事务开启 | | 拦截器不覆盖 `ExecuteUpdate/Delete` | 批量操作绕过 ChangeTracker,拦截器不触发 | | 拦截器无法修改实体字段 | `SaveChangesContext` 只读;软删除/时间戳须在应用代码中手动标记 | | 软删除 UPDATE 受查询过滤器约束 | WHERE 子句会 AND 过滤条件,防止跨租户/越权改写 | | `save_changes()` 清空跟踪器 | 后续修改需重新 `load_all()` 或 `attach()` 才能被跟踪 | | 审计拦截器需过滤自身实体 | 避免 `audit_log` 写入触发反馈循环 | | 时间戳只戳记已修改条目 | 避免未变更行产生多余 UPDATE |
下一章:[最佳实践与避坑](../11-best-practices/INDEX.md)