在实体操作前后执行自定义逻辑。钩子支持验证、规范化、副作用和授权。
快速开始
#[derive(Entity)]
#[entity(table = "users", hooks)]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub email: String,
#[field(create, response)]
pub name: String,
#[field(skip)]
pub password_hash: String,
#[field(response)]
#[auto]
pub created_at: DateTime<Utc>,
}
生成的代码
hooks 属性生成异步trait:
/// 由entity-derive生成
#[async_trait]
pub trait UserHooks: Send + Sync {
type Error: std::error::Error + Send + Sync;
/// 创建新实体前调用。
/// 修改DTO或返回错误以中止。
async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error>;
/// 实体创建后调用。
async fn after_create(&self, entity: &User) -> Result<(), Self::Error>;
/// 更新实体前调用。
/// 修改DTO或返回错误以中止。
async fn before_update(&self, id: &Uuid, dto: &mut UpdateUserRequest) -> Result<(), Self::Error>;
/// 实体更新后调用。
async fn after_update(&self, entity: &User) -> Result<(), Self::Error>;
/// 删除实体前调用。
/// 返回错误以中止。
async fn before_delete(&self, id: &Uuid) -> Result<(), Self::Error>;
/// 实体删除后调用。
async fn after_delete(&self, id: &Uuid) -> Result<(), Self::Error>;
}
实现示例
use async_trait::async_trait;
struct UserService {
pool: PgPool,
cache: RedisPool,
email_sender: EmailService,
}
#[async_trait]
impl UserHooks for UserService {
type Error = AppError;
async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error> {
// 规范化邮箱
dto.email = dto.email.trim().to_lowercase();
// 验证邮箱格式
if !dto.email.contains('@') {
return Err(AppError::Validation("邮箱格式无效".into()));
}
// 检查重复邮箱
let exists = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)"
)
.bind(&dto.email)
.fetch_one(&self.pool)
.await?;
if exists {
return Err(AppError::Conflict("邮箱已注册".into()));
}
Ok(())
}
async fn after_create(&self, entity: &User) -> Result<(), Self::Error> {
// 发送欢迎邮件
self.email_sender
.send_welcome(&entity.email, &entity.name)
.await?;
// 缓存新用户
self.cache.set(&format!("user:{}", entity.id), entity).await?;
Ok(())
}
async fn before_update(&self, id: &Uuid, dto: &mut UpdateUserRequest) -> Result<(), Self::Error> {
// 如果提供了邮箱则规范化
if let Some(ref mut email) = dto.email {
*email = email.trim().to_lowercase();
// 检查重复(排除当前用户)
let exists = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM users WHERE email = $1 AND id != $2)"
)
.bind(&*email)
.bind(id)
.fetch_one(&self.pool)
.await?;
if exists {
return Err(AppError::Conflict("邮箱已被使用".into()));
}
}
Ok(())
}
async fn after_update(&self, entity: &User) -> Result<(), Self::Error> {
// 失效缓存
self.cache.del(&format!("user:{}", entity.id)).await?;
Ok(())
}
async fn before_delete(&self, id: &Uuid) -> Result<(), Self::Error> {
// 检查用户是否可以删除
let has_orders = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = $1 AND status = 'pending')"
)
.bind(id)
.fetch_one(&self.pool)
.await?;
if has_orders {
return Err(AppError::Forbidden("无法删除有待处理订单的用户".into()));
}
Ok(())
}
async fn after_delete(&self, id: &Uuid) -> Result<(), Self::Error> {
// 失效缓存
self.cache.del(&format!("user:{}", id)).await?;
// 清理相关数据
sqlx::query("DELETE FROM user_sessions WHERE user_id = $1")
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
}
用例
验证
async fn before_create(&self, dto: &mut CreateProductRequest) -> Result<(), Self::Error> {
// 价格验证
if dto.price_cents <= 0 {
return Err(AppError::Validation("价格必须为正数".into()));
}
// SKU格式验证
if !dto.sku.chars().all(|c| c.is_alphanumeric() || c == '-') {
return Err(AppError::Validation("SKU格式无效".into()));
}
Ok(())
}
规范化
async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error> {
// 规范化邮箱
dto.email = dto.email.trim().to_lowercase();
// 规范化名字
dto.name = dto.name.trim().to_string();
// 每个单词首字母大写
dto.name = dto.name
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect::<Vec<_>>()
.join(" ");
Ok(())
}
授权
async fn before_update(&self, id: &Uuid, _dto: &mut UpdatePostRequest) -> Result<(), Self::Error> {
// 从上下文获取当前用户
let current_user = self.current_user()?;
// 检查所有权
let post = sqlx::query_as::<_, Post>(
"SELECT * FROM posts WHERE id = $1"
)
.bind(id)
.fetch_optional(&self.pool)
.await?
.ok_or(AppError::NotFound)?;
if post.author_id != current_user.id && !current_user.is_admin {
return Err(AppError::Forbidden("无法编辑其他用户的文章".into()));
}
Ok(())
}
副作用
async fn after_create(&self, entity: &Order) -> Result<(), Self::Error> {
// 更新库存
for item in &entity.items {
sqlx::query(
"UPDATE products SET stock = stock - $1 WHERE id = $2"
)
.bind(item.quantity)
.bind(item.product_id)
.execute(&self.pool)
.await?;
}
// 发送通知
self.notifications.send_order_confirmation(entity).await?;
// 调度履行任务
self.job_queue.enqueue(FulfillOrderJob { order_id: entity.id }).await?;
Ok(())
}
与软删除配合
当启用 soft_delete 时,会生成额外的钩子:
#[derive(Entity)]
#[entity(table = "documents", hooks, soft_delete)]
pub struct Document { /* ... */ }
生成的钩子:
#[async_trait]
pub trait DocumentHooks: Send + Sync {
type Error: std::error::Error + Send + Sync;
// 标准CRUD钩子...
async fn before_create(&self, dto: &mut CreateDocumentRequest) -> Result<(), Self::Error>;
async fn after_create(&self, entity: &Document) -> Result<(), Self::Error>;
async fn before_update(&self, id: &Uuid, dto: &mut UpdateDocumentRequest) -> Result<(), Self::Error>;
async fn after_update(&self, entity: &Document) -> Result<(), Self::Error>;
async fn before_delete(&self, id: &Uuid) -> Result<(), Self::Error>; // 软删除
async fn after_delete(&self, id: &Uuid) -> Result<(), Self::Error>;
// 软删除特定钩子
async fn before_restore(&self, id: &Uuid) -> Result<(), Self::Error>;
async fn after_restore(&self, entity: &Document) -> Result<(), Self::Error>;
async fn before_hard_delete(&self, id: &Uuid) -> Result<(), Self::Error>;
async fn after_hard_delete(&self, id: &Uuid) -> Result<(), Self::Error>;
}
与命令配合
当同时启用 commands 和 hooks 时,会生成命令钩子:
#[derive(Entity)]
#[entity(table = "orders", hooks, commands)]
#[command(Place)]
#[command(Cancel, requires_id)]
pub struct Order { /* ... */ }
额外的钩子:
#[async_trait]
pub trait OrderHooks: Send + Sync {
type Error: std::error::Error + Send + Sync;
// 标准CRUD钩子...
// 命令钩子
async fn before_command(&self, cmd: &OrderCommand) -> Result<(), Self::Error>;
async fn after_command(&self, cmd: &OrderCommand, result: &OrderCommandResult) -> Result<(), Self::Error>;
}
最佳实践
- 保持钩子快速 — 长时间运行的操作应该是异步任务
- 使用事务 — 将钩子 + repository调用包装在事务中
- 优雅处理错误 — 返回有意义的错误类型
- 不要重复逻辑 — 将钩子用于横切关注点
- 独立测试钩子 — 对钩子实现进行单元测试
错误处理模式
#[derive(Debug)]
pub enum HookError {
Validation(String),
Authorization(String),
Conflict(String),
Database(sqlx::Error),
}
impl std::error::Error for HookError {}
impl std::fmt::Display for HookError { /* ... */ }
impl From<sqlx::Error> for HookError {
fn from(err: sqlx::Error) -> Self {
HookError::Database(err)
}
}
#[async_trait]
impl UserHooks for UserService {
type Error = HookError;
async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error> {
if dto.email.is_empty() {
return Err(HookError::Validation("邮箱必填".into()));
}
Ok(())
}
}
另见
- [[事件|事件]] — 用于审计日志的生命周期事件
- [[命令|命令]] — 带命令钩子的CQRS模式
- [[最佳实践|最佳实践]] — 生产环境技巧