这是什么?
entity-derive 是一个 Rust 过程宏,从单一实体定义生成完整的领域层。不仅仅是 CRUD — 而是一个包含事件、钩子、命令和类型安全过滤的架构框架。
问题
一个典型的 Rust 后端项目,包含约10个实体意味着:
| 组件 | 代码行数 | 问题 |
|---|---|---|
| DTO(Create、Update、Response) | 每个实体约60行 | 手动同步,遗漏字段 |
| Repository trait + impl | 每个实体约150行 | 运行时SQL错误,复制粘贴 |
| Entity ↔ DTO 映射 | 每个实体约40行 | 数据泄露(Response中的password_hash) |
| 验证和钩子 | 分散在服务中 | 重复,没有单一来源 |
| 事件/审计 | 缺失或临时方案 | 没有变更历史 |
总计:10个实体约2500行样板代码。每次架构变更都需要在5+个地方手动编辑。
解决方案
#[derive(Entity)]
#[entity(table = "users", events, hooks, commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
#[filter(like)]
pub email: String,
#[field(skip)] // 永不泄露到API
pub password_hash: String,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
15行 → 完整的领域层:
CreateUserRequest、UpdateUserRequest、UserResponse- 类型安全SQL的
UserRepository UserEvent::Created、Updated、Deleted- 业务逻辑的
UserHooks RegisterUser、DeactivateUser命令- 用于过滤的
UserQuery
对初学者简单
最小示例 — 10行:
#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub title: String,
#[field(create, update, response)]
pub content: String,
}
完成。 你现在拥有:
CreatePostRequest、UpdatePostRequest、PostResponse- 带有
create()、find_by_id()、update()、delete()、list()的PostRepository - PostgreSQL的类型安全SQL
- 一切开箱即用
没有魔法。 运行 cargo expand — 你会看到你自己会写的代码。只是没有错误,而且只需几秒。
为什么需要事件?
问题: CRUD应用没有历史记录。谁修改了记录?什么时候?之前是什么?审计需要单独的基础设施和纪律。
解决方案: #[entity(events)] 生成类型化事件:
pub enum UserEvent {
Created(User),
Updated { id: Uuid, changes: UpdateUserRequest },
Deleted(Uuid),
}
优势:
- 开箱即用的审计 — 订阅事件,保存到日志
- 事件溯源 — 可以从事件历史恢复状态
- 集成 — Kafka、WebSocket通知、缓存失效
- 调试 — 每个实体的完整变更历史
为什么需要钩子?
问题: 业务逻辑分散各处。邮箱验证 — 在控制器中。密码哈希 — 在服务中。发送邮件 — 在单独的worker中。用户创建逻辑在哪里找?
解决方案: #[entity(hooks)] 集中生命周期:
impl UserHooks for MyHooks {
async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Error> {
dto.email = dto.email.to_lowercase(); // 规范化
validate_email(&dto.email)?; // 验证
Ok(())
}
async fn after_create(&self, user: &User) -> Result<(), Error> {
self.mailer.send_welcome(user).await?; // 业务操作
Ok(())
}
}
优势:
- 单一位置 — 所有实体逻辑都在定义旁边
- 可预测性 — 清楚什么时候执行什么
- 可测试性 — 钩子可以被模拟和独立测试
- 组合性 — 不同上下文的不同实现
为什么需要命令?
问题: REST API隐藏了意图。POST /users — 是注册?管理员创建?CSV导入?PATCH /users/123 — 停用?更改邮箱?封禁?
解决方案: #[command(...)] 表达业务领域:
#[command(Register)] // 自助注册
#[command(Invite)] // 管理员邀请
#[command(Deactivate, requires_id)] // 账户停用
#[command(Ban, requires_id)] // 违规封禁
对比:
// CRUD(发生了什么?)
pool.update(user_id, UpdateUserRequest { active: Some(false), ..default() }).await?;
// 命令(意图清晰)
handler.handle(DeactivateUser { id: user_id }).await?;
优势:
- 自文档化API — 命令名称 = 业务词汇
- 不同逻辑 —
Deactivate和Ban可以有不同的副作用 - CQRS就绪 — 命令易于路由、记录、重试
- 类型安全 — 编译器验证命令存在
为什么需要类型安全过滤?
问题: 字符串查询参数是运行时错误的来源:
GET /users?stauts=active // 拼写错误 — 静默忽略
GET /users?created_at=tomorrow // 无效日期 — 运行时panic
解决方案: #[filter] 生成类型化结构:
let query = UserQuery {
email: Some("@company.com".into()), // ILIKE '%@company.com%'
created_at_min: Some(week_ago), // >= week_ago
created_at_max: Some(now), // <= now
..Default::default()
};
let users = pool.list_filtered(&query, 100, 0).await?;
优势:
- 编译时检查 — 字段名拼写错误 = 编译错误
- 类型安全 — 不能用
String比较DateTime - 自动补全 — IDE提示可用过滤器
- SQL注入保护 — 参数绑定,不是拼接
透明性
宏不隐藏逻辑。所有生成的都是普通Rust代码,你可以:
- 阅读 —
cargo expand显示所有生成的代码 - 理解 — 没有运行时反射,只有结构体和trait
- 覆盖 —
sql = "trait"然后写你自己的SQL - 调试 — 编译器错误指向你的代码,不是宏内部
// 想了解生成了什么?
cargo expand --lib | grep -A 50 "impl UserRepository"
零魔法。 如果宏坏了 — 你总是可以手写代码。没有锁定。
Rust的全部力量
编译时保证
#[field(skip)]
pub password_hash: String,
这不是运行时检查“不要序列化这个字段“。这是字段在 UserResponse 结构中物理上不存在。不可能意外返回 — 字段根本不存在。
零成本抽象
生成的代码是:
- 没有Box/dyn的普通
struct - 直接调用sqlx,没有中间层
- 热路径上的
#[inline] - 没有超出必要的分配
基准测试: 生成的repository运行速度与手写相同。因为它就是相同的代码。
开箱即用的Async
// 一切都是async,一切都是Send + Sync
let user = pool.find_by_id(id).await?;
let users = pool.list(100, 0).await?;
与tokio、async-std、任何async运行时完全兼容。
严格类型
// 编译错误:没有这个字段
let query = UserQuery { naem: "test".into(), ..default() };
^^^^ unknown field
// 编译错误:类型错误
let query = UserQuery { created_at_min: "yesterday".into(), ..default() };
^^^^^^^^^^^^ expected DateTime<Utc>
如果代码编译了 — 它就能正确运行。
专业架构
Clean Architecture就绪
Domain Layer (entity-derive)
├── Entities — #[derive(Entity)]
├── DTOs — CreateRequest, UpdateRequest, Response
├── Repository Trait — 存储抽象
├── Events — 领域事件
├── Commands — 业务操作
└── Hooks — 生命周期逻辑
Infrastructure Layer (你的代码)
├── Repository Impl — PgPool自动或自定义
├── Event Handlers — 事件订阅
├── Command Handlers — 业务逻辑实现
└── External Services — 集成
清晰分离。Domain不知道HTTP、数据库、Kafka。这些都是实现细节。
CQRS/事件溯源就绪
// Command side
handler.handle(RegisterUser { email, name }).await?;
// Query side
let users = pool.list_filtered(&query, 100, 0).await?;
// Event side
match event {
UserEvent::Created(user) => kafka.send("user.created", &user).await?,
UserEvent::Updated { id, changes } => audit_log.record(id, changes).await?,
_ => {}
}
想要简单CRUD?有了。想要完整CQRS?启用 commands 和 events。架构随项目成长。
可扩展性
级别1:基础CRUD
#[entity(table = "users")]
级别2:+ 过滤
#[entity(table = "users")]
// + 字段上的 #[filter]
级别3:+ 事件和钩子
#[entity(table = "users", events, hooks)]
级别4:+ CQRS命令
#[entity(table = "users", events, hooks, commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]
级别5:完全控制
#[entity(table = "users", sql = "trait", events, hooks, commands)]
// 你的SQL,你的逻辑,但保留所有DTO和类型
简单开始。随着成长添加功能。不要重写 — 扩展。
安全性
#[field(skip)]
pub password_hash: String,
skip 意味着:这个字段永远不会出现在:
CreateUserRequest(不能从外部传入)UpdateUserRequest(不能通过API修改)UserResponse(不能意外返回给客户端)
与 password_hash 交互的唯一方式 — 直接通过代码中的entity。设计上不可能泄露。
为什么这很棒
| 方面 | 你得到的 |
|---|---|
| 开发速度 | 10个实体一小时而不是一天 |
| 可靠性 | 一切的编译时验证 |
| 安全性 | 不可能意外泄露数据 |
| 性能 | 零成本,像手写代码 |
| 清晰度 | 透明生成,没有魔法 |
| 灵活性 | 一个属性从简单CRUD到CQRS |
| 可扩展性 | 架构随项目成长 |
| 可维护性 | 单一真相来源,更少bug |
文档
| 主题 | 描述 |
|---|---|
| [[属性|属性]] | 完整属性参考 |
| [[过滤|过滤]] | 类型安全查询过滤 |
| [[关系|关系]] | belongs_to 和 has_many |
| [[事件|事件]] | 生命周期事件 |
| [[钩子|钩子]] | Before/after钩子 |
| [[命令|命令]] | CQRS模式 |
| [[自定义SQL|自定义SQL]] | 复杂查询 |
| [[示例|案例]] | 真实用例 |
| [[Web框架|Web框架]] | Axum、Actix集成 |
| [[最佳实践|最佳实践]] | 生产指南 |
这不是一个规定你如何生活的框架。 这是一个消除例行工作让你正确构建的工具。
属性
entity-derive 支持的所有属性完整指南。
实体级属性
通过 #[entity(...)] 应用于结构体:
#[derive(Entity)]
#[entity(
table = "users",
schema = "core",
sql = "full",
dialect = "postgres",
uuid = "v7",
soft_delete,
returning = "full",
error = "AppError",
events,
hooks,
commands
)]
pub struct User { /* ... */ }
快速参考
| 属性 | 必需 | 默认值 | 描述 |
|---|---|---|---|
table | 是 | — | 数据库表名 |
schema | 否 | "public" | 数据库模式 |
sql | 否 | "full" | SQL生成级别 |
dialect | 否 | "postgres" | 数据库方言 |
uuid | 否 | "v7" | ID生成的UUID版本 |
soft_delete | 否 | false | 启用软删除 |
returning | 否 | "full" | RETURNING子句模式 |
upsert(...) | 否 | — | 生成基于 INSERT ... ON CONFLICT 的 upsert 方法 |
api(guard = "...") | 否 | — | 在生成的 handler 中强制执行的 FromRequestParts 守卫 |
error | 否 | sqlx::Error | 自定义错误类型 |
events | 否 | false | 生成生命周期事件 |
hooks | 否 | false | 生成生命周期钩子trait |
commands | 否 | false | 启用CQRS命令模式 |
table(必需)
数据库表名。
#[entity(table = "users")] // → FROM users
#[entity(table = "user_profiles")] // → FROM user_profiles
schema(可选)
数据库模式。默认:"public"。
#[entity(table = "users")] // → FROM public.users
#[entity(table = "users", schema = "core")] // → FROM core.users
#[entity(table = "users", schema = "auth")] // → FROM auth.users
sql(可选)
SQL生成级别。默认:"full"。
| 值 | Repository Trait | PgPool实现 | 用例 |
|---|---|---|---|
"full" | 是 | 是 | 标准CRUD实体 |
"trait" | 是 | 否 | 自定义查询(joins、CTEs) |
"none" | 否 | 否 | 仅DTO,无数据库 |
#[entity(table = "users", sql = "full")] // 完全自动化(默认)
#[entity(table = "users", sql = "trait")] // 仅trait,自己实现SQL
#[entity(table = "users", sql = "none")] // 完全无数据库层
dialect(可选)
SQL生成的数据库方言。默认:"postgres"。
| 方言 | 别名 | 客户端类型 | 状态 |
|---|---|---|---|
"postgres" | "pg", "postgresql" | sqlx::PgPool | 稳定 |
"clickhouse" | "ch" | clickhouse::Client | 计划中 |
"mongodb" | "mongo" | mongodb::Client | 计划中 |
uuid(可选)
自动生成主键的UUID版本。默认:"v7"。
| 版本 | 方法 | 属性 |
|---|---|---|
"v7" | Uuid::now_v7() | 时间排序,可排序(推荐) |
"v4" | Uuid::new_v4() | 随机,广泛兼容 |
#[entity(table = "users", uuid = "v7")] // 时间排序(默认)
#[entity(table = "sessions", uuid = "v4")] // 随机UUID
为什么选择UUID v7?
- 时间排序:按创建时间自然排序
- 更好的数据库索引性能
- 无需协调(不像序列)
- 在分布式系统中全局唯一
soft_delete(可选)
启用软删除,将记录标记为已删除而不是移除它们。
#[derive(Entity)]
#[entity(table = "documents", soft_delete)]
pub struct Document {
#[id]
pub id: Uuid,
#[field(create, response)]
pub title: String,
#[field(skip)]
pub deleted_at: Option<DateTime<Utc>>, // 必需字段
}
生成的方法:
delete()— 设置deleted_at = NOW()而不是 DELETEhard_delete()— 永久删除记录restore()— 设置deleted_at = NULLfind_by_id()/list()— 自动过滤已删除的记录find_by_id_with_deleted()/list_with_deleted()— 包含已删除的记录
returning(可选)
控制INSERT/UPDATE后获取什么数据。默认:"full"。
| 模式 | SQL子句 | 用例 |
|---|---|---|
"full" | RETURNING * | 获取所有字段包括DB生成的(默认) |
"id" | RETURNING id | 确认插入,返回预构建的实体 |
"none" | (无RETURNING) | 即发即弃,最快选项 |
"col1, col2" | RETURNING col1, col2 | 返回特定列 |
#[entity(table = "logs", returning = "none")] // 最快
#[entity(table = "users", returning = "full")] // 获取DB生成的值
#[entity(table = "events", returning = "id, created_at")] // 自定义列
events(outbox)(可选)
通过事务性 outbox 实现持久事件投递。单独的 events 只生成枚举;streams 的 NOTIFY 是即发即弃。events(outbox) 使每个生成的写操作在同一事务中将序列化事件插入 entity_outbox 表,OutboxDrainer 运行时(entity-core,outbox 特性)以 FOR UPDATE SKIP LOCKED、指数退避、超过 max_attempts 后搁置的方式投递行。至少一次投递——处理器必须幂等。可与 streams 组合。
#[derive(Entity, Serialize, Deserialize)]
#[entity(table = "orders", events(outbox), migrations)]
pub struct Order { /* ... */ }
sqlx::query(Order::MIGRATION_OUTBOX).execute(&pool).await?;
struct Notifier;
#[async_trait::async_trait]
impl entity_core::outbox::OutboxHandler for Notifier {
type Error = anyhow::Error;
async fn handle(&self, row: &OutboxRow) -> Result<(), Self::Error> {
deliver(&row.entity, &row.payload).await
}
}
entity_core::outbox::OutboxDrainer::new(pool, Notifier).run().await;
upsert(...)(可选)
生成基于 INSERT ... ON CONFLICT 的 upsert 仓库方法。
#[derive(Entity)]
#[entity(table = "users", upsert(conflict = "email"))]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, response)]
#[column(unique)]
pub email: String,
#[field(create, update, response)]
pub name: String,
}
| 选项 | 必需 | 默认值 | 说明 |
|---|---|---|---|
conflict | 是 | — | 逗号分隔的冲突目标列 |
action | 否 | "update" | "update"(DO UPDATE)或 "nothing"(DO NOTHING) |
生成内容:
action = "update"→async fn upsert(&self, dto: CreateUserRequest) -> Result<User, Error>— 用新值覆盖所有非冲突列(DO UPDATE SET col = EXCLUDED.col)并返回持久化后的行action = "nothing"→async fn upsert(&self, dto: CreateUserRequest) -> Result<Option<User>, Error>— 保留现有行不变;None表示冲突行已存在
编译期校验:
- 冲突列必须存在且具有唯一性保证(
#[id]、#[column(unique)]或匹配的unique_index(...)) - 要求
returning = "full"(默认值) action = "update"至少需要一个非冲突的可更新列
启用 streams 时,upsert 会为每个返回的行发布 Created 通知。
api(guard = "...")(可选)
在生成的 handler 中强制执行认证。security = "..." 仅在 OpenAPI 中做文档标注;guard 会将 axum 的 FromRequestParts extractor 作为每个生成的 CRUD 和命令 handler 的首个参数注入——提取失败会在 handler 主体执行前拒绝请求。
pub struct RequireAuth;
impl<S: Send + Sync> FromRequestParts<S> for RequireAuth {
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
parts.headers.contains_key("authorization")
.then_some(Self)
.ok_or(StatusCode::UNAUTHORIZED)
}
}
#[derive(Entity)]
#[entity(table = "users", api(tag = "Users", handlers, guard = "RequireAuth", guard(list = "none")))]
pub struct User { /* ... */ }
按操作覆盖:guard(create = "Admin", list = "none", ...),操作包括 create、get、update、delete、list、commands;字面量 "none" 禁用守卫。列在 public = [...] 中的命令不会收到守卫。
error(可选)
repository的自定义错误类型。默认:sqlx::Error。
#[derive(Debug)]
pub enum AppError {
Database(sqlx::Error),
NotFound,
Validation(String),
}
impl std::error::Error for AppError {}
impl std::fmt::Display for AppError { /* ... */ }
// 必需:从sqlx::Error转换
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
AppError::Database(err)
}
}
#[derive(Entity)]
#[entity(table = "users", error = "AppError")]
pub struct User { /* ... */ }
// 生成的repository使用AppError:
// impl UserRepository for PgPool {
// type Error = AppError;
// ...
// }
events(可选)
生成生命周期事件枚举。详见 [[事件|事件]]。
#[entity(table = "orders", events)]
生成:
pub enum OrderEvent {
Created(Order),
Updated { id: Uuid, changes: UpdateOrderRequest },
Deleted(Uuid),
}
hooks(可选)
生成生命周期钩子trait。详见 [[钩子|钩子]]。
#[entity(table = "users", hooks)]
生成:
#[async_trait]
pub trait UserHooks: Send + Sync {
type Error: std::error::Error + Send + Sync;
async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error>;
async fn after_create(&self, entity: &User) -> Result<(), Self::Error>;
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>;
}
commands(可选)
启用CQRS命令模式。详见 [[命令|命令]]。
#[entity(table = "users", commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]
字段级属性
应用于单个字段。
#[id]
标记主键字段。
行为:
- 自动生成UUID(默认v7,可用
uuid属性配置) - 始终包含在
ResponseDTO中 - 从
CreateRequest和UpdateRequest中排除
#[id]
pub id: Uuid,
#[auto]
标记自动生成的字段(时间戳、序列)。
行为:
- 在
From<CreateRequest>中获取Default::default() - 从
CreateRequest和UpdateRequest中排除 - 可通过
#[field(response)]包含在Response中
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[field(...)]
控制DTO包含。组合多个选项:
#[field(create)] // 仅在CreateRequest中
#[field(update)] // 仅在UpdateRequest中
#[field(response)] // 仅在Response中
#[field(create, response)] // 在Create和Response中
#[field(create, update, response)] // 在所有三个中
#[field(skip)] // 从所有DTO中排除
create
在 CreateRequest DTO中包含字段。
#[field(create)]
pub email: String,
// 生成:
pub struct CreateUserRequest {
pub email: String,
}
update
在 UpdateRequest DTO中包含字段。
重要: 非可选字段会自动包装在 Option<T> 中以支持部分更新。
#[field(update)]
pub name: String, // 非Option
// 生成:
pub struct UpdateUserRequest {
pub name: Option<String>, // 自动包装
}
response
在 Response DTO中包含字段。
#[field(response)]
pub email: String,
// 生成:
pub struct UserResponse {
pub id: Uuid, // 始终包含(有#[id])
pub email: String, // 包含
}
skip
从所有DTO中排除字段。用于敏感数据。
#[field(skip)]
pub password_hash: String,
重要: skip 覆盖所有其他字段选项。字段仅存在于:
- 原始实体结构
Row结构(用于数据库读取)Insertable结构(用于数据库写入)
#[column(pg_enum = "...")]
将 ValueObject Postgres 枚举接入 DDL 生成。
#[derive(ValueObject, Debug, Clone, Serialize, Deserialize)]
#[value_object(pg_type = "order_status", sqlx)]
pub enum OrderStatus { Pending, Shipped, Delivered }
#[derive(Entity)]
#[entity(table = "orders", migrations)]
pub struct Order {
#[id]
pub id: Uuid,
#[field(create, update, response)]
#[column(pg_enum = "order_status")]
pub status: OrderStatus,
}
for ddl in Order::MIGRATION_TYPES {
sqlx::query(ddl).execute(&pool).await?;
}
sqlx::query(Order::MIGRATION_UP).execute(&pool).await?;
- 设置 DDL 列类型(否则枚举字段回退为 TEXT)
- 将枚举的幂等
PG_CREATE_TYPEDDL 注册到{Entity}::MIGRATION_TYPES— 请在MIGRATION_UP之前执行 - 声明的名称在编译期与枚举的
PG_TYPE常量核对,不一致会导致构建失败 ValueObject的可选sqlx标志会生成sqlx::Type/Encode/Decode实现;若已自行 derivesqlx::Type则省略
#[owner]
行级所有权范围限定。标记承载所有者 id 的列后,仓库将获得范围限定方法——绝不泄露某行是否属于其他所有者,并遵循 soft_delete。
#[derive(Entity)]
#[entity(table = "orders")]
pub struct Order {
#[id]
pub id: Uuid,
#[owner]
pub user_id: Uuid,
#[field(create, update, response)]
pub note: String,
}
let mine = pool.list_by_owner(user_id, 20, 0).await?;
let order = pool.find_by_id_scoped(id, user_id).await?;
let updated = pool.update_scoped(id, user_id, patch).await?;
let removed = pool.delete_scoped(id, user_id).await?;
生成方法:find_by_id_scoped、list_by_owner、update_scoped(存在 update 字段时;行不属于该所有者则返回 None)、delete_scoped。#[owner] 字段最多一个;与 #[id] 组合会在编译期被拒绝。
#[filter] / #[filter(...)]
生成查询过滤字段。详见 [[过滤|过滤]]。
#[filter] // 精确匹配:WHERE field = $n
#[filter(eq)] // 同上
#[filter(like)] // 模式匹配:WHERE field ILIKE $n
#[filter(range)] // 范围:WHERE field >= $n AND field <= $m
#[belongs_to(Entity)]
外键关系。详见 [[关系|关系]]。
#[belongs_to(User)]
pub user_id: Uuid,
生成: repository中的 find_user() 方法。
#[has_many(Entity)]
一对多关系(实体级)。详见 [[关系|关系]]。
#[has_many(Post)]
pub struct User { /* ... */ }
生成: repository中的 find_posts() 方法。
#[projection(Name: fields)]
生成部分视图结构(实体级)。
#[projection(Public: id, name, avatar)]
#[projection(Admin: id, name, email, role)]
pub struct User { /* ... */ }
生成:
UserPublic { id, name, avatar }UserAdmin { id, name, email, role }From<User>实现find_by_id_public()、find_by_id_admin()方法
命令属性
通过 #[command(...)] 应用于实体级。
快速参考
| 语法 | 效果 |
|---|---|
#[command(Name)] | 使用所有 #[field(create)] 字段 |
#[command(Name: field1, field2)] | 仅使用指定字段(添加 requires_id) |
#[command(Name, requires_id)] | 添加ID字段,无其他字段 |
#[command(Name, source = "create")] | 显式使用create字段(默认) |
#[command(Name, source = "update")] | 使用update字段(可选,添加 requires_id) |
#[command(Name, source = "none")] | 无payload字段 |
#[command(Name, payload = "Type")] | 使用自定义payload结构 |
#[command(Name, result = "Type")] | 使用自定义结果类型 |
#[command(Name, kind = "create")] | 提示:创建实体(默认) |
#[command(Name, kind = "update")] | 提示:修改实体 |
#[command(Name, kind = "delete")] | 提示:删除实体(返回 ()) |
#[command(Name, kind = "custom")] | 提示:自定义操作 |
详见 [[命令|命令]]。
完整示例
#[derive(Entity)]
#[entity(
table = "posts",
schema = "blog",
sql = "full",
dialect = "postgres",
uuid = "v7",
soft_delete,
returning = "full",
events,
hooks,
commands
)]
#[has_many(Comment)]
#[projection(Summary: id, title, author_id, created_at)]
#[command(Publish)]
#[command(Archive, requires_id)]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
#[filter(like)]
pub title: String,
#[field(create, update, response)]
pub content: String,
#[field(create, response)]
#[belongs_to(User)]
#[filter]
pub author_id: Uuid,
#[field(update, response)]
pub published: bool,
#[field(response)]
#[filter(range)]
pub view_count: i64,
#[field(skip)]
pub moderation_notes: String,
#[field(skip)]
pub deleted_at: Option<DateTime<Utc>>,
#[auto]
#[field(response)]
#[filter(range)]
pub created_at: DateTime<Utc>,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
决策矩阵
| 我想要… | 属性 |
|---|---|
| 自动生成主键 | #[id] |
| 使用随机UUID | 实体上 uuid = "v4" |
| 使用时间排序UUID | uuid = "v7"(默认) |
| 在POST请求体中接受 | #[field(create)] |
| 在PATCH请求体中接受 | #[field(update)] |
| 在API响应中返回 | #[field(response)] |
| 接受并返回 | #[field(create, update, response)] |
| 从所有API隐藏 | #[field(skip)] |
| 自动生成时间戳 | #[auto] + #[field(response)] |
| 只读(DB管理) | 仅 #[field(response)] |
| 只写(不返回) | 仅 #[field(create)] |
| 自定义SQL查询 | sql = "trait" |
| 仅DTO,无DB | sql = "none" |
| 软删除记录 | 实体上 soft_delete |
| 自定义错误类型 | 实体上 error = "MyError" |
| 按精确值过滤 | 字段上 #[filter] |
| 按模式过滤 | 字段上 #[filter(like)] |
| 按范围过滤 | 字段上 #[filter(range)] |
| 跟踪实体变更 | 实体上 events |
| 在生命周期运行代码 | 实体上 hooks |
| 使用领域命令 | 实体上 commands + #[command(...)] |
| 定义关系 | #[belongs_to(Entity)] 或 #[has_many(Entity)] |
| 实体部分视图 | #[projection(Name: fields)] |
更新 DTO:PATCH 语义
生成的更新是真正的部分补丁:SET 子句在运行时由实际存在的字段构建,省略的字段保持不变。可空列通过 entity_core::serde_helpers::double_option 使用双重 Option(None = 保留,Some(None) = 置 NULL,Some(Some(v)) = 置 v)。
// {} → nothing changes
// {"nickname": null} → nickname = NULL
// {"nickname": "neo"} → nickname = 'neo'
let patch: UpdateProfileRequest = serde_json::from_str(body)?;
let profile = pool.update(id, patch).await?;
migrations(...) 选项
除简单标志外,migrations 还接受 DDL 选项:touch_updated_at(共享 plpgsql 函数 + 每表 BEFORE UPDATE 触发器保持 updated_at 最新;需要 updated_at 字段,编译期校验)、audit(entity_audit_log 表 + 记录 to_jsonb(OLD/NEW) 差异的触发器)和 extensions = "pg_trgm, pgcrypto"(幂等 CREATE EXTENSION)。新常量:MIGRATION_TRIGGERS(在 MIGRATION_UP 之后执行)、MIGRATION_EXTENSIONS(之前执行)。
#[entity(table = "articles", migrations(touch_updated_at, audit, extensions = "pg_trgm"))]
pub struct Article { /* ... */ }
for ddl in Article::MIGRATION_EXTENSIONS { sqlx::query(ddl).execute(&pool).await?; }
sqlx::query(Article::MIGRATION_UP).execute(&pool).await?;
for ddl in Article::MIGRATION_TRIGGERS { sqlx::query(ddl).execute(&pool).await?; }
#[version]
乐观锁。标记一个整数列(i16/i32/i64)后,更新 DTO 获得必填的 expected_version 字段,生成的 UPDATE 会递增该列,且仅在存储的版本仍匹配时生效——过期的写入会以冲突错误失败,而不是覆盖较新的数据。DDL 默认 INTEGER NOT NULL DEFAULT 0。适用于普通、范围限定和事务更新。
#[derive(Entity)]
#[entity(table = "orders", migrations)]
pub struct Order {
#[id] pub id: Uuid,
#[field(create, update, response)] pub note: String,
#[version] #[field(response)] #[auto] pub version: i32,
}
let patch = UpdateOrderRequest { note: Some("v2".into()), expected_version: order.version };
let updated = pool.update(order.id, patch).await?;
typed_constraints(可选)
宏了解自己创建的每个约束。启用此标志后,生成的写入方法会解析被违反的约束名称(unique 列、belongs_to 外键、列 check、unique_index 名称),并返回 entity_core::ConstraintError { kind, constraint, field },而不是原始驱动错误。需要实现 From<ConstraintError> 的自定义 error 类型;不启用标志则行为不变。
#[entity(table = "users", typed_constraints, error = "AppError")]
pub struct User {
#[id] pub id: Uuid,
#[field(create, response)] #[column(unique)] pub email: String,
}
match pool.create(dto).await {
Err(AppError::Constraint(v)) if v.field == Some("email") => conflict_409(),
other => other?,
}
#[embed(prefix = "...", fields(...))]
将值对象展开为带前缀的平面标量列。DDL、Row 结构体、CRUD SQL 和动态 PATCH 更新在 price_amount_cents / price_currency 上操作,而 DTO 和实体携带结构体本身。声明的形状在编译期与真实结构体进行解构对照——重命名、改类型、缺失或多余的字段都会导致构建失败。暂不支持 Option<T> 父字段。
pub struct Money { pub amount_cents: i64, pub currency: String }
#[derive(Entity)]
#[entity(table = "products", migrations)]
pub struct Product {
#[id] pub id: Uuid,
#[field(create, update, response)]
#[embed(prefix = "price_", fields(amount_cents: i64, currency: String))]
pub price: Money,
}
garde 特性(验证后端)
在生成的 DTO 上启用 garde::Validate,作为 validator 的持续维护替代。字段的 #[validate(...)] 规则(length、range、email、url、pattern)会翻译为 garde 语法;无约束字段获得 garde(skip);更新 DTO 的 Option 包装通过 inner(...) 验证内部值。两个特性同时启用时,validate 优先。
#[field(create, update, response)]
#[validate(length(min = 3, max = 8))]
pub name: String,
let dto: CreateUserRequest = serde_json::from_str(body)?;
garde::Validate::validate(&dto)?;
constraint(...)(可选,需配合 typed_constraints)
声明宏无法推断的约束——基于自然键的外键、自定义命名的 CHECK 约束、手写迁移中的索引——使违规解析为携带所声明字段的 ConstraintError。类型:unique、foreign_key、check。自定义条目优先于同名的派生条目。需要 typed_constraints。
#[entity(
table = "orders",
typed_constraints,
constraint(name = "orders_currency_fkey", kind = "foreign_key", field = "currency"),
constraint(name = "orders_window_check", kind = "check"),
)]
事务性 upsert
同时启用 transactions 和 upsert(...) 时,{Entity}TransactionRepo 适配器提供与池方法相同 SQL 语义的 upsert,在事务句柄上执行——适用于 upsert 必须与相邻语句共享原子性的流程。
let mut tx = pool.begin().await?;
sqlx::query("UPDATE users SET username = NULL WHERE ...").execute(&mut *tx).await?;
let user = UserTransactionRepo::new(&mut tx).upsert(dto).await?;
tx.commit().await?;
案例
常见用例的实用示例。
用户管理
带身份验证字段的经典用户实体:
use entity_derive::Entity;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Entity)]
#[entity(table = "users", schema = "auth")]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub username: String,
#[field(create, update, response)]
pub email: String,
#[field(create, update)] // 接受但从不返回
pub password_hash: String,
#[field(response)]
pub email_verified: bool,
#[field(update, response)]
pub avatar_url: Option<String>,
#[field(response)]
pub role: String,
#[field(skip)] // 内部审计字段
pub last_login_ip: Option<String>,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
用法:
// 创建用户
let request = CreateUserRequest {
username: "john_doe".into(),
email: "john@example.com".into(),
password_hash: hash_password("secret123"),
};
let user = pool.create(request).await?;
// 更新用户
let update = UpdateUserRequest {
avatar_url: Some(Some("https://cdn.example.com/avatar.jpg".into())),
..Default::default()
};
let user = pool.update(user.id, update).await?;
// 响应是安全的 - 没有 password_hash,没有 last_login_ip
let response = UserResponse::from(&user);
博客系统
带作者关系的文章:
#[derive(Entity)]
#[entity(table = "posts", schema = "blog")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub title: String,
#[field(create, update, response)]
pub slug: String,
#[field(create, update, response)]
pub content: String,
#[field(create, update, response)]
pub excerpt: Option<String>,
#[field(create, response)] // 只设置一次,不能更改作者
pub author_id: Uuid,
#[field(update, response)]
pub published: bool,
#[field(update, response)]
pub published_at: Option<DateTime<Utc>>,
#[field(response)] // 只读,由触发器管理
pub view_count: i64,
#[field(skip)] // 内部审核
pub moderation_status: String,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
多对多分类:
#[derive(Entity)]
#[entity(table = "categories", schema = "blog")]
pub struct Category {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(create, update, response)]
pub slug: String,
#[field(create, update, response)]
pub description: Option<String>,
#[field(response)]
pub post_count: i64, // 计算字段
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
电商系统
产品目录:
#[derive(Entity)]
#[entity(table = "products", schema = "catalog")]
pub struct Product {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(create, update, response)]
pub sku: String,
#[field(create, update, response)]
pub description: Option<String>,
#[field(create, update, response)]
pub price_cents: i64,
#[field(create, update, response)]
pub currency: String,
#[field(update, response)]
pub stock_quantity: i32,
#[field(update, response)]
pub is_active: bool,
#[field(create, response)]
pub category_id: Uuid,
#[field(skip)] // 内部成本跟踪
pub cost_cents: i64,
#[field(skip)] // 供应商信息
pub supplier_id: Option<Uuid>,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
带状态跟踪的订单:
#[derive(Entity)]
#[entity(table = "orders", schema = "sales")]
pub struct Order {
#[id]
pub id: Uuid,
#[field(create, response)]
pub customer_id: Uuid,
#[field(response)]
pub order_number: String, // 由数据库序列生成
#[field(update, response)]
pub status: String,
#[field(create, response)]
pub total_cents: i64,
#[field(create, response)]
pub currency: String,
#[field(create, update, response)]
pub shipping_address: String,
#[field(update, response)]
pub tracking_number: Option<String>,
#[field(skip)] // 支付处理器数据
pub payment_intent_id: Option<String>,
#[field(skip)] // 内部备注
pub admin_notes: Option<String>,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
多租户SaaS
组织范围内的实体:
#[derive(Entity)]
#[entity(table = "organizations", schema = "tenants")]
pub struct Organization {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(create, response)]
pub slug: String, // 创建后不可变
#[field(update, response)]
pub plan: String,
#[field(response)]
pub member_count: i32,
#[field(skip)] // 账单信息
pub stripe_customer_id: Option<String>,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
#[derive(Entity)]
#[entity(table = "projects", schema = "tenants")]
pub struct Project {
#[id]
pub id: Uuid,
#[field(create, response)] // 只设置一次
pub organization_id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(create, update, response)]
pub description: Option<String>,
#[field(update, response)]
pub archived: bool,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
仅DTO(无数据库)
用于没有持久化的API契约:
#[derive(Entity)]
#[entity(table = "webhooks", sql = "none")]
pub struct WebhookPayload {
#[id]
pub id: Uuid,
#[field(create, response)]
pub event_type: String,
#[field(create, response)]
pub payload: String,
#[field(create, response)]
pub timestamp: DateTime<Utc>,
#[field(response)]
pub signature: String,
}
这只生成:
CreateWebhookPayloadRequestWebhookPayloadResponseFrom实现
没有repository,没有SQL,没有Row/Insertable结构体。
仅自定义查询
当标准CRUD不够用时:
#[derive(Entity)]
#[entity(table = "analytics_events", schema = "analytics", sql = "trait")]
pub struct AnalyticsEvent {
#[id]
pub id: Uuid,
#[field(create, response)]
pub event_name: String,
#[field(create, response)]
pub user_id: Option<Uuid>,
#[field(create, response)]
pub properties: serde_json::Value,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
// 自己实现自定义查询:
impl AnalyticsEventRepository for PgPool {
type Error = sqlx::Error;
async fn create(&self, dto: CreateAnalyticsEventRequest) -> Result<AnalyticsEvent, Self::Error> {
// 批量插入、分区表等
}
async fn find_by_id(&self, id: Uuid) -> Result<Option<AnalyticsEvent>, Self::Error> {
// 带时间分区的查询
}
// 超出CRUD的自定义方法:
async fn aggregate_by_event(&self, start: DateTime<Utc>, end: DateTime<Utc>)
-> Result<Vec<EventAggregate>, Self::Error> {
// 复杂聚合查询
}
}
另见
- [[属性|属性]] — 完整属性参考
- [[自定义SQL|自定义SQL]] — 自定义repository实现
- [[Web框架|Web框架]] — Axum/Actix集成
批量操作
每个仓库都提供批量原语:find_by_ids(WHERE id = ANY($1),单次往返)、原子的 create_many(单个事务,任一行失败则整批回滚)以及感知软删除的 delete_many(返回实际受影响的行数)。启用事件投递(streams 或 outbox)时,会在同一事务内按行发出事件。
let posts: Vec<Post> = pool.find_by_ids(ids).await?;
let created: Vec<Post> = pool.create_many(dtos).await?;
let removed: u64 = pool.delete_many(stale_ids).await?;
过滤
生成类型安全的查询结构体用于过滤实体。过滤支持分页、搜索和范围查询,具有编译时安全性。
快速开始
#[derive(Entity)]
#[entity(table = "products")]
pub struct Product {
#[id]
pub id: Uuid,
#[field(create, update, response)]
#[filter]
pub name: String,
#[field(create, update, response)]
#[filter(like)]
pub description: String,
#[field(create, update, response)]
#[filter(range)]
pub price: i64,
#[field(create, response)]
#[filter]
pub category_id: Uuid,
#[field(response)]
#[auto]
#[filter(range)]
pub created_at: DateTime<Utc>,
}
生成的代码
查询结构体
/// 用于过滤Product实体的查询参数。
#[derive(Debug, Clone, Default)]
pub struct ProductQuery {
/// 按精确name匹配过滤。
pub name: Option<String>,
/// 按description模式过滤(ILIKE)。
pub description: Option<String>,
/// 按最低价格过滤。
pub price_from: Option<i64>,
/// 按最高价格过滤。
pub price_to: Option<i64>,
/// 按精确category_id匹配过滤。
pub category_id: Option<Uuid>,
/// 按最早created_at过滤。
pub created_at_from: Option<DateTime<Utc>>,
/// 按最晚created_at过滤。
pub created_at_to: Option<DateTime<Utc>>,
/// 最大结果数。
pub limit: Option<i64>,
/// 跳过的结果数。
pub offset: Option<i64>,
}
Repository方法
#[async_trait]
pub trait ProductRepository: Send + Sync {
// ... 标准CRUD方法
/// 使用过滤器查询产品。
async fn query(&self, query: ProductQuery) -> Result<Vec<Product>, Self::Error>;
}
生成的SQL
SELECT id, name, description, price, category_id, created_at
FROM products
WHERE ($1 IS NULL OR name = $1)
AND ($2 IS NULL OR description ILIKE $2)
AND ($3 IS NULL OR price >= $3)
AND ($4 IS NULL OR price <= $4)
AND ($5 IS NULL OR category_id = $5)
AND ($6 IS NULL OR created_at >= $6)
AND ($7 IS NULL OR created_at <= $7)
ORDER BY created_at DESC
LIMIT $8 OFFSET $9
过滤器类型
精确匹配 (#[filter] 或 #[filter(eq)])
过滤字段等于提供值的记录。
#[filter]
pub status: String,
#[filter(eq)] // 同上
pub category_id: Uuid,
生成:
pub status: Option<String>,
pub category_id: Option<Uuid>,
SQL:
WHERE status = $1
AND category_id = $2
模式匹配 (#[filter(like)])
使用不区分大小写的模式匹配(ILIKE)过滤。
#[filter(like)]
pub name: String,
#[filter(like)]
pub description: String,
生成:
pub name: Option<String>,
pub description: Option<String>,
SQL:
WHERE name ILIKE $1
AND description ILIKE $2
用法:
let query = ProductQuery {
name: Some("%widget%".into()), // 包含 "widget"
description: Some("premium%".into()), // 以 "premium" 开头
..Default::default()
};
范围过滤 (#[filter(range)])
在范围内过滤(包含边界)。
#[filter(range)]
pub price: i64,
#[filter(range)]
pub created_at: DateTime<Utc>,
生成:
pub price_from: Option<i64>,
pub price_to: Option<i64>,
pub created_at_from: Option<DateTime<Utc>>,
pub created_at_to: Option<DateTime<Utc>>,
SQL:
WHERE price >= $1 AND price <= $2
AND created_at >= $3 AND created_at <= $4
使用示例
基本过滤
// 按类别查找产品
let query = ProductQuery {
category_id: Some(electronics_category_id),
..Default::default()
};
let products = repo.query(query).await?;
分页
// 获取第2页(每页20项)
let query = ProductQuery {
limit: Some(20),
offset: Some(20),
..Default::default()
};
let products = repo.query(query).await?;
组合过滤器
// 搜索价格实惠的电子产品
let query = ProductQuery {
category_id: Some(electronics_category_id),
price_from: Some(0),
price_to: Some(10000), // $100.00
name: Some("%phone%".into()),
limit: Some(50),
..Default::default()
};
let products = repo.query(query).await?;
日期范围
// 获取本月创建的产品
let now = Utc::now();
let month_start = now.with_day(1).unwrap().date_naive().and_hms_opt(0, 0, 0).unwrap();
let query = ProductQuery {
created_at_from: Some(month_start.and_utc()),
created_at_to: Some(now),
..Default::default()
};
let products = repo.query(query).await?;
API端点集成
use axum::{extract::Query, Json};
#[derive(Deserialize)]
pub struct ProductQueryParams {
pub name: Option<String>,
pub category_id: Option<Uuid>,
pub min_price: Option<i64>,
pub max_price: Option<i64>,
pub page: Option<i64>,
pub per_page: Option<i64>,
}
async fn list_products(
Query(params): Query<ProductQueryParams>,
pool: Extension<PgPool>,
) -> Result<Json<Vec<ProductResponse>>, AppError> {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(20).min(100);
let query = ProductQuery {
name: params.name.map(|n| format!("%{}%", n)),
category_id: params.category_id,
price_from: params.min_price,
price_to: params.max_price,
limit: Some(per_page),
offset: Some((page - 1) * per_page),
..Default::default()
};
let products = pool.query(query).await?;
let responses: Vec<_> = products.into_iter().map(ProductResponse::from).collect();
Ok(Json(responses))
}
与软删除配合
当启用 soft_delete 时,查询自动排除已删除的记录:
#[derive(Entity)]
#[entity(table = "documents", soft_delete)]
pub struct Document {
#[id]
pub id: Uuid,
#[field(create, response)]
#[filter(like)]
pub title: String,
#[field(skip)]
pub deleted_at: Option<DateTime<Utc>>,
}
生成的SQL:
SELECT * FROM documents
WHERE deleted_at IS NULL
AND ($1 IS NULL OR title ILIKE $1)
LIMIT $2 OFFSET $3
包含已删除记录的附加方法:
async fn query_with_deleted(&self, query: DocumentQuery) -> Result<Vec<Document>, Self::Error>;
自定义查询扩展
对于复杂查询,使用 sql = "trait" 并实现自定义过滤:
#[derive(Entity)]
#[entity(table = "products", sql = "trait")]
pub struct Product { /* ... */ }
pub trait ProductQueryExt {
async fn search_fulltext(&self, term: &str, limit: i64) -> Result<Vec<Product>, sqlx::Error>;
async fn find_by_tags(&self, tags: &[String]) -> Result<Vec<Product>, sqlx::Error>;
}
#[async_trait]
impl ProductQueryExt for PgPool {
async fn search_fulltext(&self, term: &str, limit: i64) -> Result<Vec<Product>, sqlx::Error> {
let rows: Vec<ProductRow> = sqlx::query_as(
r#"
SELECT * FROM products
WHERE to_tsvector('english', name || ' ' || description)
@@ plainto_tsquery('english', $1)
ORDER BY ts_rank(to_tsvector('english', name || ' ' || description),
plainto_tsquery('english', $1)) DESC
LIMIT $2
"#
)
.bind(term)
.bind(limit)
.fetch_all(self)
.await?;
Ok(rows.into_iter().map(Product::from).collect())
}
async fn find_by_tags(&self, tags: &[String]) -> Result<Vec<Product>, sqlx::Error> {
let rows: Vec<ProductRow> = sqlx::query_as(
"SELECT * FROM products WHERE tags && $1"
)
.bind(tags)
.fetch_all(self)
.await?;
Ok(rows.into_iter().map(Product::from).collect())
}
}
最佳实践
- 默认分页 — 始终应用合理的限制以防止大结果集
- 验证模式 — 清理LIKE模式以防止SQL问题
- 索引过滤列 — 为经常过滤的字段创建数据库索引
- 使用特定过滤器 — 尽可能优先使用精确匹配而非模式匹配
- 与排序结合 — 考虑向查询结构体添加排序字段
另见
- [[属性|属性]] — 完整属性参考
- [[自定义SQL|自定义SQL]] — 复杂自定义查询
- [[关系|关系]] — 带关系的过滤
排序与 keyset 分页
用 #[sort] 标记可排序列:Query 结构体获得白名单选择器 {Entity}SortField(每列一对 Asc/Desc 变体,JSON 为 snake_case),用户输入永远无法注入 SQL。每个仓库还会生成 list_after keyset 分页——使用默认 UUIDv7 id 时,按 id 的遍历在时间上稳定,且与 OFFSET 不同,深页不会变慢。
#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
#[sort]
#[filter(like)]
pub title: String,
#[field(create, response)]
#[sort]
pub views: i64,
}
let query = PostQuery {
sort: Some(PostSortField::ViewsDesc),
limit: Some(20),
..Default::default()
};
let top: Vec<Post> = pool.query(query).await?;
let page: Vec<Post> = pool.list_after(None, 20).await?;
let next: Vec<Post> = pool.list_after(page.last().map(|p| p.id), 20).await?;
三元组搜索
在文本列上使用 #[filter(search)] 会添加模糊子串过滤(col ILIKE '%' || $n || '%',搜索词通过绑定传入)。启用 migrations 时,相应的 gin_trgm_ops 索引会进入 MIGRATION_UP,并且 pg_trgm 会自动加入 MIGRATION_EXTENSIONS。编译期检查:字段必须是 String。
#[field(create, update, response)]
#[filter(search)]
pub title: String,
let hits = pool.query(ArticleQuery { title: Some("rust".into()), ..Default::default() }).await?;
关系
使用 #[belongs_to] 和 #[has_many] 定义实体之间的关系。关系在repository中生成类型安全的导航方法。
快速开始
// 父实体
#[derive(Entity)]
#[entity(table = "users")]
#[has_many(Post)]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
}
// 子实体
#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, response)]
#[belongs_to(User)]
pub user_id: Uuid,
#[field(create, update, response)]
pub title: String,
}
生成的代码
User上的 #[has_many(Post)]
#[async_trait]
impl UserRepository for PgPool {
// ... 标准CRUD方法
/// 查找属于此用户的所有文章。
async fn find_posts(&self, user_id: Uuid) -> Result<Vec<Post>, Self::Error> {
let rows: Vec<PostRow> = sqlx::query_as(
"SELECT * FROM posts WHERE user_id = $1 ORDER BY created_at DESC"
)
.bind(&user_id)
.fetch_all(self)
.await?;
Ok(rows.into_iter().map(Post::from).collect())
}
}
Post上的 #[belongs_to(User)]
#[async_trait]
impl PostRepository for PgPool {
// ... 标准CRUD方法
/// 查找此文章所属的用户。
async fn find_user(&self, id: Uuid) -> Result<Option<User>, Self::Error> {
// 首先获取文章以找到user_id
let post = self.find_by_id(id).await?;
if let Some(post) = post {
let row: Option<UserRow> = sqlx::query_as(
"SELECT * FROM users WHERE id = $1"
)
.bind(&post.user_id)
.fetch_optional(self)
.await?;
Ok(row.map(User::from))
} else {
Ok(None)
}
}
}
关系类型
belongs_to(多对一)
子实体通过外键引用父实体。
#[derive(Entity)]
#[entity(table = "comments")]
pub struct Comment {
#[id]
pub id: Uuid,
#[field(create, response)]
#[belongs_to(Post)]
pub post_id: Uuid,
#[field(create, response)]
#[belongs_to(User)]
pub author_id: Uuid,
#[field(create, response)]
pub content: String,
}
生成的方法:
find_post(comment_id)→Option<Post>find_author(comment_id)→Option<User>(注:方法名从字段名派生,去掉_id)
has_many(一对多)
父实体拥有多个子实体。
#[derive(Entity)]
#[entity(table = "users")]
#[has_many(Post)]
#[has_many(Comment)]
pub struct User {
#[id]
pub id: Uuid,
// ...
}
生成的方法:
find_posts(user_id)→Vec<Post>find_comments(user_id)→Vec<Comment>
使用示例
加载关联数据
// 获取用户及其文章
async fn get_user_with_posts(
pool: &PgPool,
user_id: Uuid,
) -> Result<Option<(User, Vec<Post>)>, sqlx::Error> {
let user = pool.find_by_id(user_id).await?;
if let Some(user) = user {
let posts = pool.find_posts(user_id).await?;
Ok(Some((user, posts)))
} else {
Ok(None)
}
}
// 获取文章及作者
async fn get_post_with_author(
pool: &PgPool,
post_id: Uuid,
) -> Result<Option<(Post, User)>, sqlx::Error> {
let post = pool.find_by_id(post_id).await?;
if let Some(post) = post {
let user = pool.find_user(post.id).await?;
if let Some(user) = user {
return Ok(Some((post, user)));
}
}
Ok(None)
}
构建响应DTO
#[derive(Serialize)]
pub struct PostWithAuthor {
#[serde(flatten)]
pub post: PostResponse,
pub author: UserResponse,
}
async fn get_posts_with_authors(
pool: &PgPool,
limit: i64,
) -> Result<Vec<PostWithAuthor>, sqlx::Error> {
let posts = pool.list(limit, 0).await?;
let mut results = Vec::with_capacity(posts.len());
for post in posts {
if let Some(user) = pool.find_user(post.id).await? {
results.push(PostWithAuthor {
post: PostResponse::from(&post),
author: UserResponse::from(&user),
});
}
}
Ok(results)
}
多重关系
一个实体可以有多个关系:
#[derive(Entity)]
#[entity(table = "organizations")]
#[has_many(User)]
#[has_many(Project)]
#[has_many(Team)]
pub struct Organization {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
}
#[derive(Entity)]
#[entity(table = "projects")]
pub struct Project {
#[id]
pub id: Uuid,
#[field(create, response)]
#[belongs_to(Organization)]
pub organization_id: Uuid,
#[field(create, response)]
#[belongs_to(User)]
pub owner_id: Uuid,
#[field(create, update, response)]
pub name: String,
}
Organization生成:
find_users(org_id)find_projects(org_id)find_teams(org_id)
Project生成:
find_organization(project_id)find_owner(project_id)
使用 sql = “trait” 自定义Join
对于需要eager loading的复杂查询,使用自定义SQL:
#[derive(Entity)]
#[entity(table = "posts", sql = "trait")]
pub struct Post { /* ... */ }
pub struct PostWithRelations {
pub post: Post,
pub author: User,
pub comments: Vec<Comment>,
}
pub trait PostRepositoryExt {
async fn find_with_relations(&self, id: Uuid) -> Result<Option<PostWithRelations>, sqlx::Error>;
async fn list_with_authors(&self, limit: i64) -> Result<Vec<(Post, User)>, sqlx::Error>;
}
#[async_trait]
impl PostRepositoryExt for PgPool {
async fn find_with_relations(&self, id: Uuid) -> Result<Option<PostWithRelations>, sqlx::Error> {
// 带join的单次查询
let row = sqlx::query_as::<_, (PostRow, UserRow)>(
r#"
SELECT p.*, u.*
FROM posts p
JOIN users u ON u.id = p.user_id
WHERE p.id = $1
"#
)
.bind(&id)
.fetch_optional(self)
.await?;
if let Some((post_row, user_row)) = row {
let comments: Vec<CommentRow> = sqlx::query_as(
"SELECT * FROM comments WHERE post_id = $1 ORDER BY created_at"
)
.bind(&id)
.fetch_all(self)
.await?;
Ok(Some(PostWithRelations {
post: Post::from(post_row),
author: User::from(user_row),
comments: comments.into_iter().map(Comment::from).collect(),
}))
} else {
Ok(None)
}
}
async fn list_with_authors(&self, limit: i64) -> Result<Vec<(Post, User)>, sqlx::Error> {
let rows = sqlx::query_as::<_, (PostRow, UserRow)>(
r#"
SELECT p.*, u.*
FROM posts p
JOIN users u ON u.id = p.user_id
ORDER BY p.created_at DESC
LIMIT $1
"#
)
.bind(limit)
.fetch_all(self)
.await?;
Ok(rows.into_iter()
.map(|(p, u)| (Post::from(p), User::from(u)))
.collect())
}
}
与过滤结合
将关系与查询过滤结合:
#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, response)]
#[belongs_to(User)]
#[filter] // 启用按user_id过滤
pub user_id: Uuid,
#[field(create, update, response)]
#[filter(like)]
pub title: String,
#[field(response)]
#[auto]
#[filter(range)]
pub created_at: DateTime<Utc>,
}
用法:
// 获取特定用户的文章并按标题过滤
let query = PostQuery {
user_id: Some(user_id),
title: Some("%rust%".into()),
limit: Some(20),
..Default::default()
};
let posts = pool.query(query).await?;
最佳实践
- 避免N+1查询 — 在获取多个关联实体时使用join进行eager loading
- 使用分页 — 始终限制
has_many结果 - 考虑数据访问模式 — 在外键列上添加索引
- 适当缓存 — 缓存经常访问的关联数据
- 使用投影 — 只获取关联实体的必要字段
另见
- [[过滤|过滤]] — 查询过滤
- [[自定义SQL|自定义SQL]] — 复杂join和查询
- [[最佳实践|最佳实践]] — 性能技巧
has_many through(多对多)
通过 through 声明连接表,仓库即可获得基于 JOIN 的查询和链接管理;migrations 会生成 MIGRATION_JUNCTIONS(两个外键上的复合主键,两侧均 ON DELETE CASCADE)。
#[derive(Entity)]
#[entity(table = "teams", migrations)]
#[has_many(User, through = "team_members")]
pub struct Team { /* ... */ }
for ddl in Team::MIGRATION_JUNCTIONS {
sqlx::query(ddl).execute(&pool).await?;
}
pool.add_user(team_id, user_id).await?;
let members: Vec<User> = pool.find_users(team_id).await?;
let linked = pool.has_user(team_id, user_id).await?;
let removed = pool.remove_user(team_id, user_id).await?;
生成的方法:find_users(INNER JOIN)、add_user(幂等,ON CONFLICT DO NOTHING)、remove_user(未链接时返回 false)、has_user(SELECT EXISTS)。
事件
为实体生命周期变更生成领域事件。事件支持审计日志、事件溯源和消息队列集成。
快速开始
#[derive(Entity)]
#[entity(table = "orders", events)]
pub struct Order {
#[id]
pub id: Uuid,
#[field(create, response)]
pub customer_id: Uuid,
#[field(create, update, response)]
pub status: String,
#[field(create, response)]
pub total_cents: i64,
#[field(response)]
#[auto]
pub created_at: DateTime<Utc>,
}
生成的代码
events 属性生成事件枚举:
/// 由entity-derive生成
#[derive(Debug, Clone)]
pub enum OrderEvent {
/// 实体已创建。
Created(Order),
/// 实体已更新。
Updated {
id: Uuid,
changes: UpdateOrderRequest,
},
/// 实体已删除。
Deleted(Uuid),
}
使用示例
基本事件发布
use async_trait::async_trait;
#[async_trait]
pub trait EventBus: Send + Sync {
async fn publish<E: Send + Sync>(&self, event: E);
}
async fn create_order(
repo: &impl OrderRepository,
bus: &impl EventBus,
dto: CreateOrderRequest,
) -> Result<Order, sqlx::Error> {
let order = repo.create(dto).await?;
// 创建成功后发布事件
bus.publish(OrderEvent::Created(order.clone())).await;
Ok(order)
}
async fn update_order(
repo: &impl OrderRepository,
bus: &impl EventBus,
id: Uuid,
dto: UpdateOrderRequest,
) -> Result<Order, sqlx::Error> {
let order = repo.update(id, dto.clone()).await?;
bus.publish(OrderEvent::Updated { id, changes: dto }).await;
Ok(order)
}
async fn delete_order(
repo: &impl OrderRepository,
bus: &impl EventBus,
id: Uuid,
) -> Result<bool, sqlx::Error> {
let deleted = repo.delete(id).await?;
if deleted {
bus.publish(OrderEvent::Deleted(id)).await;
}
Ok(deleted)
}
审计日志
struct AuditLogger {
pool: PgPool,
}
#[async_trait]
impl EventHandler<OrderEvent> for AuditLogger {
async fn handle(&self, event: OrderEvent) {
let (action, entity_id, details) = match &event {
OrderEvent::Created(order) => (
"created",
order.id,
serde_json::to_string(order).unwrap(),
),
OrderEvent::Updated { id, changes } => (
"updated",
*id,
serde_json::to_string(changes).unwrap(),
),
OrderEvent::Deleted(id) => (
"deleted",
*id,
String::new(),
),
};
sqlx::query(
"INSERT INTO audit_log (entity_type, entity_id, action, details, created_at)
VALUES ('order', $1, $2, $3, NOW())"
)
.bind(entity_id)
.bind(action)
.bind(details)
.execute(&self.pool)
.await
.ok();
}
}
消息队列集成
use rdkafka::producer::FutureProducer;
struct KafkaEventBus {
producer: FutureProducer,
topic: String,
}
#[async_trait]
impl EventBus for KafkaEventBus {
async fn publish<E: Serialize + Send + Sync>(&self, event: E) {
let payload = serde_json::to_vec(&event).unwrap();
self.producer
.send(
FutureRecord::to(&self.topic)
.payload(&payload)
.key(&Uuid::new_v4().to_string()),
Duration::from_secs(5),
)
.await
.ok();
}
}
事件溯源模式
struct OrderAggregate {
events: Vec<OrderEvent>,
current_state: Option<Order>,
}
impl OrderAggregate {
fn apply(&mut self, event: OrderEvent) {
match &event {
OrderEvent::Created(order) => {
self.current_state = Some(order.clone());
}
OrderEvent::Updated { changes, .. } => {
if let Some(ref mut order) = self.current_state {
if let Some(status) = &changes.status {
order.status = status.clone();
}
}
}
OrderEvent::Deleted(_) => {
self.current_state = None;
}
}
self.events.push(event);
}
fn replay(events: Vec<OrderEvent>) -> Self {
let mut aggregate = Self {
events: Vec::new(),
current_state: None,
};
for event in events {
aggregate.apply(event);
}
aggregate
}
}
与软删除配合
当启用 soft_delete 时,会生成额外的事件:
#[derive(Entity)]
#[entity(table = "documents", events, soft_delete)]
pub struct Document {
#[id]
pub id: Uuid,
#[field(create, response)]
pub title: String,
#[field(skip)]
pub deleted_at: Option<DateTime<Utc>>,
}
生成:
pub enum DocumentEvent {
Created(Document),
Updated { id: Uuid, changes: UpdateDocumentRequest },
Deleted(Uuid), // 软删除
Restored(Uuid), // 从软删除恢复
HardDeleted(Uuid), // 永久删除
}
最佳实践
- 提交后发布 — 只在数据库事务成功后发布事件
- 幂等处理器 — 事件处理器应该是幂等的,以支持至少一次交付
- 包含上下文 — 考虑添加元数据(user_id、timestamp、correlation_id)
- 异步处理 — 使用后台worker处理重型事件
- 死信队列 — 优雅地处理失败的事件
与钩子结合
事件和钩子配合良好:
#[derive(Entity)]
#[entity(table = "orders", events, hooks)]
pub struct Order { /* ... */ }
struct OrderService {
repo: PgPool,
bus: EventBus,
}
#[async_trait]
impl OrderHooks for OrderService {
type Error = AppError;
async fn after_create(&self, entity: &Order) -> Result<(), Self::Error> {
// 在钩子中发布事件
self.bus.publish(OrderEvent::Created(entity.clone())).await;
Ok(())
}
async fn after_update(&self, entity: &Order) -> Result<(), Self::Error> {
// 事件也可以在这里发布
Ok(())
}
async fn after_delete(&self, id: &Uuid) -> Result<(), Self::Error> {
self.bus.publish(OrderEvent::Deleted(*id)).await;
Ok(())
}
}
另见
- [[钩子|钩子]] — 在生命周期事件上执行自定义逻辑
- [[命令|命令]] — 带命令事件的CQRS模式
- [[最佳实践|最佳实践]] — 生产环境技巧
流
使用 Postgres LISTEN/NOTIFY 实时订阅实体变更。流支持实时仪表板、即时通知、缓存失效和事件驱动架构。
快速开始
#[derive(Entity, Serialize, Deserialize)]
#[entity(table = "orders", events, streams)]
pub struct Order {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub status: String,
#[field(create, response)]
pub customer_id: Uuid,
}
要求:
- 实体必须派生
Serialize和Deserialize(用于 JSON 载荷) - 需要同时使用
events和streams属性 - 在 Cargo.toml 中启用
streams特性
[dependencies]
entity-derive = { version = "0.3", features = ["postgres", "streams"] }
serde = { version = "1", features = ["derive"] }
生成的代码
streams 属性生成:
频道常量
impl Order {
/// Postgres NOTIFY 频道名称。
pub const CHANNEL: &'static str = "entity_orders";
}
订阅者结构体
/// Order 实时变更订阅者。
pub struct OrderSubscriber {
listener: PgListener,
}
impl OrderSubscriber {
/// 连接并订阅频道。
pub async fn new(pool: &PgPool) -> Result<Self, sqlx::Error>;
/// 等待下一个事件(阻塞)。
pub async fn recv(&mut self) -> Result<OrderEvent, StreamError<sqlx::Error>>;
/// 非阻塞检查事件。
pub async fn try_recv(&mut self) -> Result<Option<OrderEvent>, StreamError<sqlx::Error>>;
}
自动通知
CRUD 操作自动发出事件:
// 在生成的 create() 方法中:
async fn create(&self, dto: CreateOrderRequest) -> Result<Order, Self::Error> {
let order = /* insert */;
// 自动生成的通知
let event = OrderEvent::created(order.clone());
let payload = serde_json::to_string(&event)?;
sqlx::query("SELECT pg_notify($1, $2)")
.bind(Order::CHANNEL)
.bind(&payload)
.execute(self)
.await?;
Ok(order)
}
使用示例
基本订阅
use entity_derive::StreamError;
async fn watch_orders(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
let mut subscriber = OrderSubscriber::new(pool).await?;
loop {
match subscriber.recv().await {
Ok(event) => {
match event {
OrderEvent::Created(order) => {
println!("新订单: {}", order.id);
}
OrderEvent::Updated { old, new } => {
println!("订单 {} 已更新: {} -> {}", new.id, old.status, new.status);
}
OrderEvent::HardDeleted { id } => {
println!("订单 {} 已删除", id);
}
_ => {}
}
}
Err(StreamError::Database(e)) => {
eprintln!("数据库错误: {}", e);
break;
}
Err(StreamError::Deserialize(e)) => {
eprintln!("无效的事件载荷: {}", e);
}
}
}
Ok(())
}
实时仪表板(Axum WebSocket)
use axum::{
extract::{State, WebSocketUpgrade, ws::{Message, WebSocket}},
response::IntoResponse,
};
async fn ws_handler(
ws: WebSocketUpgrade,
State(pool): State<PgPool>,
) -> impl IntoResponse {
ws.on_upgrade(|socket| handle_socket(socket, pool))
}
async fn handle_socket(mut socket: WebSocket, pool: PgPool) {
let mut subscriber = match OrderSubscriber::new(&pool).await {
Ok(s) => s,
Err(_) => return,
};
loop {
match subscriber.recv().await {
Ok(event) => {
let json = serde_json::to_string(&event).unwrap();
if socket.send(Message::Text(json)).await.is_err() {
break;
}
}
Err(_) => break,
}
}
}
缓存失效
struct CacheInvalidator {
cache: Redis,
pool: PgPool,
}
impl CacheInvalidator {
async fn run(&self) -> Result<(), StreamError<sqlx::Error>> {
let mut subscriber = OrderSubscriber::new(&self.pool).await
.map_err(StreamError::Database)?;
loop {
let event = subscriber.recv().await?;
let key = format!("order:{}", event.entity_id());
match event {
OrderEvent::Created(_) | OrderEvent::Updated { .. } => {
self.cache.del(&key).await.ok();
}
OrderEvent::HardDeleted { id } | OrderEvent::SoftDeleted { id } => {
self.cache.del(&format!("order:{}", id)).await.ok();
}
_ => {}
}
}
}
}
带优雅关闭的后台工作器
use tokio::sync::watch;
async fn notification_worker(
pool: PgPool,
mut shutdown: watch::Receiver<bool>,
) {
let mut subscriber = OrderSubscriber::new(&pool).await.unwrap();
loop {
tokio::select! {
result = subscriber.recv() => {
match result {
Ok(event) => process_event(event).await,
Err(e) => {
eprintln!("流错误: {:?}", e);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
_ = shutdown.changed() => {
println!("正在关闭通知工作器");
break;
}
}
}
}
错误处理
use entity_derive::StreamError;
match subscriber.recv().await {
Ok(event) => { /* 处理 */ }
Err(StreamError::Database(sqlx_error)) => {
// 连接丢失、查询失败等
// 订阅者将在下次 recv() 时自动重连
}
Err(StreamError::Deserialize(message)) => {
// 无效的 JSON 载荷
// 记录日志并继续 - 不要中断循环
}
}
架构
CRUD 操作 (create/update/delete)
│
▼
pg_notify(channel, event_json)
│
▼
Postgres NOTIFY
│
┌────┴────┐
▼ ▼
订阅者 订阅者 (多个监听器)
│ │
▼ ▼
WebSocket 缓存
仪表板 失效器
最佳实践
- 重连 — PgListener 自动重连;设计循环以处理临时故障
- 幂等性 — 事件可能多次传递;处理程序应该是幂等的
- 载荷大小 — 保持实体小巧;大载荷可能超出 Postgres 限制
- 独立连接池 — 为监听器使用专用连接池
- 监控 — 记录流错误并跟踪事件处理延迟
- 优雅关闭 — 使用 select! 配合关闭信号来清理资源
配合软删除
启用 soft_delete 时,可使用额外事件:
#[derive(Entity, Serialize, Deserialize)]
#[entity(table = "documents", events, streams, soft_delete)]
pub struct Document {
#[id]
pub id: Uuid,
#[field(create, response)]
pub title: String,
#[field(skip)]
pub deleted_at: Option<DateTime<Utc>>,
}
// 事件包括:
// - DocumentEvent::SoftDeleted { id }
// - DocumentEvent::Restored { id }
// - DocumentEvent::HardDeleted { id }
另请参阅
- [[事件|事件]] — 无实时流的事件枚举
- [[钩子|钩子]] — 在生命周期事件上执行自定义逻辑
- [[最佳实践|最佳实践]] — 生产环境技巧
钩子
在实体操作前后执行自定义逻辑。钩子支持验证、规范化、副作用和授权。
快速开始
#[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模式
- [[最佳实践|最佳实践]] — 生产环境技巧
命令
定义面向业务的命令而不是通用CRUD。命令将领域语言带入你的API,并启用命令查询职责分离(CQRS)模式。
快速开始
#[derive(Entity)]
#[entity(table = "users", commands)]
#[command(Register)]
#[command(UpdateEmail: email)]
#[command(Deactivate, requires_id)]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub email: String,
#[field(create, response)]
pub name: String,
#[field(response)]
pub active: bool,
}
生成的代码
命令结构体
/// User上Register操作的命令载荷。
#[derive(Debug, Clone)]
pub struct RegisterUser {
pub email: String,
pub name: String,
}
/// User上UpdateEmail操作的命令载荷。
#[derive(Debug, Clone)]
pub struct UpdateEmailUser {
pub id: Uuid,
pub email: String,
}
/// User上Deactivate操作的命令载荷。
#[derive(Debug, Clone)]
pub struct DeactivateUser {
pub id: Uuid,
}
命令枚举
/// User实体的命令枚举。
#[derive(Debug, Clone)]
pub enum UserCommand {
Register(RegisterUser),
UpdateEmail(UpdateEmailUser),
Deactivate(DeactivateUser),
}
impl EntityCommand for UserCommand {
fn kind(&self) -> CommandKind {
match self {
UserCommand::Register(_) => CommandKind::Create,
UserCommand::UpdateEmail(_) => CommandKind::Update,
UserCommand::Deactivate(_) => CommandKind::Custom,
}
}
fn name(&self) -> &'static str {
match self {
UserCommand::Register(_) => "Register",
UserCommand::UpdateEmail(_) => "UpdateEmail",
UserCommand::Deactivate(_) => "Deactivate",
}
}
}
结果枚举
/// User命令执行的结果枚举。
#[derive(Debug, Clone)]
pub enum UserCommandResult {
Register(User),
UpdateEmail(User),
Deactivate,
}
Handler Trait
/// 处理User命令的异步trait。
#[async_trait]
pub trait UserCommandHandler: Send + Sync {
type Error: std::error::Error + Send + Sync;
type Context: Send + Sync;
/// 将命令分派到适当的处理器。
async fn handle(&self, cmd: UserCommand, ctx: &Self::Context)
-> Result<UserCommandResult, Self::Error>;
/// 处理Register命令。
async fn handle_register(&self, cmd: RegisterUser, ctx: &Self::Context)
-> Result<User, Self::Error>;
/// 处理UpdateEmail命令。
async fn handle_update_email(&self, cmd: UpdateEmailUser, ctx: &Self::Context)
-> Result<User, Self::Error>;
/// 处理Deactivate命令。
async fn handle_deactivate(&self, cmd: DeactivateUser, ctx: &Self::Context)
-> Result<(), Self::Error>;
}
命令语法参考
基本命令
使用所有 #[field(create)] 字段:
#[command(Register)]
// 生成:RegisterUser { email, name }
特定字段
仅使用指定字段(自动添加 requires_id):
#[command(UpdateEmail: email)]
// 生成:UpdateEmailUser { id, email }
#[command(UpdateProfile: name, bio, avatar)]
// 生成:UpdateProfileUser { id, name, bio, avatar }
仅ID命令
只添加ID字段:
#[command(Deactivate, requires_id)]
// 生成:DeactivateUser { id }
#[command(Delete, requires_id, kind = "delete")]
// 生成:DeleteUser { id },返回 ()
自定义载荷
使用外部结构体:
pub struct TransferPayload {
pub from_account: Uuid,
pub to_account: Uuid,
pub amount: i64,
}
#[command(Transfer, payload = "TransferPayload")]
// 直接使用TransferPayload
自定义结果
使用自定义结果类型:
pub struct TransferResult {
pub transaction_id: Uuid,
pub success: bool,
}
#[command(Transfer, payload = "TransferPayload", result = "TransferResult")]
// 返回TransferResult而不是实体
Source选项
控制使用哪些字段:
#[command(Create, source = "create")] // 使用#[field(create)]字段(默认)
#[command(Modify, source = "update")] // 使用#[field(update)]字段(可选)
#[command(Ping, source = "none")] // 无载荷字段
Kind提示
影响结果类型推断:
#[command(Create, kind = "create")] // 返回实体(默认)
#[command(Update, kind = "update")] // 返回实体
#[command(Remove, kind = "delete")] // 返回 ()
#[command(Process, kind = "custom")] // 从source推断
实现示例
use async_trait::async_trait;
struct UserHandler {
pool: PgPool,
email_service: EmailService,
}
struct RequestContext {
user_id: Option<Uuid>,
correlation_id: Uuid,
}
#[async_trait]
impl UserCommandHandler for UserHandler {
type Error = AppError;
type Context = RequestContext;
async fn handle(&self, cmd: UserCommand, ctx: &Self::Context)
-> Result<UserCommandResult, Self::Error>
{
match cmd {
UserCommand::Register(c) => {
let user = self.handle_register(c, ctx).await?;
Ok(UserCommandResult::Register(user))
}
UserCommand::UpdateEmail(c) => {
let user = self.handle_update_email(c, ctx).await?;
Ok(UserCommandResult::UpdateEmail(user))
}
UserCommand::Deactivate(c) => {
self.handle_deactivate(c, ctx).await?;
Ok(UserCommandResult::Deactivate)
}
}
}
async fn handle_register(&self, cmd: RegisterUser, ctx: &Self::Context)
-> Result<User, Self::Error>
{
// 验证
if cmd.email.is_empty() {
return Err(AppError::Validation("邮箱必填".into()));
}
// 创建用户
let user = User {
id: Uuid::now_v7(),
email: cmd.email.to_lowercase(),
name: cmd.name,
active: true,
};
// 持久化
sqlx::query(
"INSERT INTO users (id, email, name, active) VALUES ($1, $2, $3, $4)"
)
.bind(user.id)
.bind(&user.email)
.bind(&user.name)
.bind(user.active)
.execute(&self.pool)
.await?;
// 副作用
self.email_service.send_welcome(&user.email).await?;
Ok(user)
}
async fn handle_update_email(&self, cmd: UpdateEmailUser, ctx: &Self::Context)
-> Result<User, Self::Error>
{
// 授权检查
if ctx.user_id != Some(cmd.id) {
return Err(AppError::Forbidden("无法更新其他用户的邮箱".into()));
}
// 更新
let user: User = sqlx::query_as(
"UPDATE users SET email = $1 WHERE id = $2 RETURNING *"
)
.bind(&cmd.email.to_lowercase())
.bind(cmd.id)
.fetch_one(&self.pool)
.await?;
// 发送验证
self.email_service.send_verification(&user.email).await?;
Ok(user)
}
async fn handle_deactivate(&self, cmd: DeactivateUser, ctx: &Self::Context)
-> Result<(), Self::Error>
{
sqlx::query("UPDATE users SET active = false WHERE id = $1")
.bind(cmd.id)
.execute(&self.pool)
.await?;
Ok(())
}
}
使用命令
async fn register_user(
handler: &impl UserCommandHandler,
email: String,
name: String,
) -> Result<User, AppError> {
let cmd = RegisterUser { email, name };
let ctx = RequestContext {
user_id: None,
correlation_id: Uuid::new_v4(),
};
match handler.handle(UserCommand::Register(cmd), &ctx).await? {
UserCommandResult::Register(user) => Ok(user),
_ => unreachable!(),
}
}
// 或直接调用特定handler
async fn update_email(
handler: &impl UserCommandHandler,
user_id: Uuid,
new_email: String,
ctx: &RequestContext,
) -> Result<User, AppError> {
let cmd = UpdateEmailUser {
id: user_id,
email: new_email,
};
handler.handle_update_email(cmd, ctx).await
}
EntityCommand Trait
所有命令枚举都实现 EntityCommand trait:
use entity_derive::{EntityCommand, CommandKind};
let cmd = UserCommand::Register(register_data);
// 获取命令元数据
assert_eq!(cmd.name(), "Register");
assert!(matches!(cmd.kind(), CommandKind::Create));
// 模式匹配
match cmd.kind() {
CommandKind::Create => println!("创建实体"),
CommandKind::Update => println!("更新实体"),
CommandKind::Delete => println!("删除实体"),
CommandKind::Custom => println!("自定义操作"),
}
命令钩子
当同时启用 commands 和 hooks 时:
#[derive(Entity)]
#[entity(table = "orders", commands, hooks)]
#[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>;
}
最佳实践
- 领域语言 — 使用业务术语:
RegisterUser而不是CreateUser - 单一职责 — 一个命令 = 一个业务操作
- 明确意图 — 命令名应描述操作
- 在handler中验证 — 将验证逻辑保留在命令handler中
- 尽可能幂等 — 设计命令以便安全重试
- 使用上下文 — 通过上下文传递请求元数据(用户、关联ID)
CQRS模式
命令是CQRS的一半。与投影结合用于查询端:
#[derive(Entity)]
#[entity(table = "orders", commands)]
#[projection(Summary: id, status, total_cents, created_at)]
#[projection(Details: id, status, items, shipping_address, total_cents)]
#[command(Place)]
#[command(Ship, requires_id)]
#[command(Cancel, requires_id)]
pub struct Order { /* ... */ }
// 命令(写入端)
let result = handler.handle(OrderCommand::Place(place_order), &ctx).await?;
// 查询(读取端)
let summary = repo.find_by_id_summary(order_id).await?;
let details = repo.find_by_id_details(order_id).await?;
另见
- [[钩子|钩子]] — 生命周期钩子包括命令钩子
- [[事件|事件]] — 用于审计日志的事件生成
- [[属性|属性]] — 完整属性参考
事务
具有自动提交/回滚的类型安全多实体事务。
什么是事务?
数据库事务是一种将多个数据库操作组合成单个原子单元的方法。这意味着:
- 全有或全无:要么所有操作都成功,要么没有任何操作被应用
- 自动回滚:如果任何操作失败,所有先前的更改都会自动撤销
- 数据一致性:您的数据库永远不会处于不一致状态
为什么需要事务?
想象一下您正在构建一个银行应用程序,需要在账户之间转账:
1. 从账户A减去100元
2. 向账户B增加100元
没有事务,如果步骤1成功但步骤2失败(网络错误、数据库崩溃等),您刚刚丢失了100元!钱从A中减去了,但从未添加到B。
使用事务,如果步骤2失败,步骤1会自动回滚。钱留在账户A中,就像什么都没发生一样。
启用事务
将transactions属性添加到您的实体:
use entity_derive::Entity;
use uuid::Uuid;
#[derive(Entity)]
#[entity(table = "accounts", transactions)] // <- 添加这个
pub struct Account {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub user_id: Uuid,
#[field(create, update, response)]
pub balance: i64,
}
生成了什么?
对于带有#[entity(transactions)]的Account实体,宏会生成:
1. 事务仓库适配器
pub struct AccountTransactionRepo<'t> {
tx: &'t mut sqlx::Transaction<'static, sqlx::Postgres>,
}
这就像您的常规仓库一样,但所有操作都在事务内部发生。
2. 构建器扩展特征
pub trait TransactionWithAccount<'p> {
fn with_accounts(self) -> Transaction<'p, PgPool, AccountTransactionRepo<'static>>;
}
这将with_accounts()方法添加到事务构建器中。
可用方法
在事务内部,您可以访问这些方法:
| 方法 | 签名 | 描述 |
|---|---|---|
create | create(dto) -> Result<Entity, Error> | 插入新记录 |
find_by_id | find_by_id(id) -> Result<Option<Entity>, Error> | 按主键查找 |
update | update(id, dto) -> Result<Entity, Error> | 更新现有记录 |
delete | delete(id) -> Result<bool, Error> | 删除记录(或软删除) |
list | list(limit, offset) -> Result<Vec<Entity>, Error> | 分页列表 |
基本示例
use entity_core::prelude::*;
async fn create_account(pool: &PgPool, user_id: Uuid) -> Result<Account, AppError> {
Transaction::new(pool) // 1. 开始构建事务
.with_accounts() // 2. 添加Account仓库
.run(|mut ctx| async move { // 3. 执行操作
let account = ctx.accounts().create(CreateAccountRequest {
user_id,
balance: 0,
}).await?;
Ok(account) // 4. 返回结果(自动提交)
})
.await
}
逐步说明:
Transaction::new(pool)— 使用数据库池创建新的事务构建器.with_accounts()— 将Account仓库添加到事务上下文.run(|mut ctx| async move { ... })— 在事务内执行操作Ok(account)— 返回Ok提交事务。返回Err回滚事务。
完整示例:转账
此示例展示了事务的全部功能:
use entity_core::prelude::*;
use uuid::Uuid;
#[derive(Debug)]
pub enum TransferError {
Database(sqlx::Error),
AccountNotFound(Uuid),
InsufficientFunds { available: i64, requested: i64 },
}
impl From<sqlx::Error> for TransferError {
fn from(e: sqlx::Error) -> Self {
TransferError::Database(e)
}
}
impl From<TransactionError<sqlx::Error>> for TransferError {
fn from(e: TransactionError<sqlx::Error>) -> Self {
TransferError::Database(e.into_inner())
}
}
/// 原子地在两个账户之间转账。
///
/// 如果任何步骤失败,所有更改都会自动回滚。
pub async fn transfer(
pool: &PgPool,
from_id: Uuid,
to_id: Uuid,
amount: i64,
) -> Result<(), TransferError> {
Transaction::new(pool)
.with_accounts()
.run(|mut ctx| async move {
// 步骤1:获取源账户
let from = ctx.accounts()
.find_by_id(from_id)
.await?
.ok_or(TransferError::AccountNotFound(from_id))?;
// 步骤2:检查是否有足够的钱
if from.balance < amount {
return Err(TransferError::InsufficientFunds {
available: from.balance,
requested: amount,
});
}
// 步骤3:获取目标账户
let to = ctx.accounts()
.find_by_id(to_id)
.await?
.ok_or(TransferError::AccountNotFound(to_id))?;
// 步骤4:从源账户减去
// 如果这成功但步骤5失败,这将被回滚
ctx.accounts().update(from_id, UpdateAccountRequest {
balance: Some(from.balance - amount),
user_id: None,
}).await?;
// 步骤5:添加到目标账户
ctx.accounts().update(to_id, UpdateAccountRequest {
balance: Some(to.balance + amount),
user_id: None,
}).await?;
// 所有操作成功 - 事务将提交
Ok(())
})
.await
}
不同场景下会发生什么:
| 场景 | 结果 |
|---|---|
| 两次更新都成功 | 事务提交,转账完成 |
| 源账户未找到 | 事务回滚(无更改) |
| 余额不足 | 事务回滚(无更改) |
| 第一次更新成功,第二次失败 | 事务回滚(第一次更新被撤销!) |
| 事务中途网络错误 | 事务回滚(无部分更改) |
一个事务中的多个实体
您可以原子地操作多个实体:
#[derive(Entity)]
#[entity(table = "accounts", transactions)]
pub struct Account {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub balance: i64,
}
#[derive(Entity)]
#[entity(table = "transfer_logs", transactions)]
pub struct TransferLog {
#[id]
pub id: Uuid,
#[field(create, response)]
pub from_account_id: Uuid,
#[field(create, response)]
pub to_account_id: Uuid,
#[field(create, response)]
pub amount: i64,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
async fn transfer_with_logging(
pool: &PgPool,
from_id: Uuid,
to_id: Uuid,
amount: i64,
) -> Result<TransferLog, AppError> {
Transaction::new(pool)
.with_accounts() // 添加Account仓库
.with_transfer_logs() // 添加TransferLog仓库
.run(|mut ctx| async move {
// 更新余额
let from = ctx.accounts().find_by_id(from_id).await?
.ok_or(AppError::NotFound)?;
ctx.accounts().update(from_id, UpdateAccountRequest {
balance: Some(from.balance - amount),
}).await?;
let to = ctx.accounts().find_by_id(to_id).await?
.ok_or(AppError::NotFound)?;
ctx.accounts().update(to_id, UpdateAccountRequest {
balance: Some(to.balance + amount),
}).await?;
// 创建日志条目 - 全部在同一事务中!
let log = ctx.transfer_logs().create(CreateTransferLogRequest {
from_account_id: from_id,
to_account_id: to_id,
amount,
}).await?;
Ok(log)
})
.await
}
如果日志创建失败,两个账户更新都会回滚!
错误处理
自动回滚
从闭包返回的任何错误都会触发回滚:
Transaction::new(pool)
.with_accounts()
.run(|mut ctx| async move {
ctx.accounts().update(id, dto).await?; // 成功
// 某些验证失败
if amount < 0 {
return Err(AppError::InvalidAmount); // <- 触发回滚!
}
// 这永远不会执行,上面的更新会被撤销
ctx.accounts().update(other_id, other_dto).await?;
Ok(())
})
.await
事务错误类型
TransactionError枚举告诉您出了什么问题:
use entity_core::transaction::TransactionError;
let result = Transaction::new(pool)
.with_accounts()
.run(|mut ctx| async move { /* ... */ })
.await;
match result {
Ok(value) => {
println!("成功:{:?}", value);
}
Err(e) => {
if e.is_begin() {
println!("启动事务失败");
} else if e.is_operation() {
println!("操作失败:{}", e);
} else if e.is_commit() {
println!("提交失败");
} else if e.is_rollback() {
println!("回滚失败");
}
// 获取内部数据库错误
let db_error: sqlx::Error = e.into_inner();
}
}
与软删除一起使用
事务遵守soft_delete属性:
#[derive(Entity)]
#[entity(table = "documents", transactions, soft_delete)]
pub struct Document {
#[id]
pub id: Uuid,
#[field(create, response)]
pub title: String,
#[field(skip)]
pub deleted_at: Option<DateTime<Utc>>, // soft_delete必需
}
async fn archive_document(pool: &PgPool, id: Uuid) -> Result<bool, AppError> {
Transaction::new(pool)
.with_documents()
.run(|mut ctx| async move {
// 这设置deleted_at = NOW()而不是DELETE
let deleted = ctx.documents().delete(id).await?;
Ok(deleted)
})
.await
}
最佳实践
1. 保持事务简短
错误示例:长时间运行的事务
Transaction::new(pool)
.with_accounts()
.run(|mut ctx| async move {
let account = ctx.accounts().find_by_id(id).await?;
// 不要这样做:在事务内调用外部API
let rate = external_api.get_exchange_rate().await?; // <- 慢!
ctx.accounts().update(id, dto).await?;
Ok(())
})
.await
正确示例:在外部进行慢操作
// 在启动事务之前获取外部数据
let rate = external_api.get_exchange_rate().await?;
Transaction::new(pool)
.with_accounts()
.run(|mut ctx| async move {
ctx.accounts().update(id, UpdateAccountRequest {
balance: Some(calculate_new_balance(rate)),
}).await?;
Ok(())
})
.await
2. 不要为单个操作使用事务
不必要:
Transaction::new(pool)
.with_users()
.run(|mut ctx| async move {
ctx.users().find_by_id(id).await // 只有一个操作!
})
.await
更好:使用常规仓库
pool.find_by_id(id).await // 不需要事务
3. 正确处理所有错误
始终使用?传播错误:
Transaction::new(pool)
.with_accounts()
.run(|mut ctx| async move {
let result = ctx.accounts().update(id, dto).await;
// 不要这样做:吞掉错误
if let Err(e) = result {
log::error!("更新失败:{}", e);
// 事务不会正确回滚!
}
// 这样做:传播错误
ctx.accounts().update(id, dto).await?; // <- 使用?
Ok(())
})
.await
常见模式
检查然后更新
Transaction::new(pool)
.with_products()
.run(|mut ctx| async move {
let product = ctx.products().find_by_id(id).await?
.ok_or(AppError::NotFound)?;
if product.stock < quantity {
return Err(AppError::OutOfStock);
}
ctx.products().update(id, UpdateProductRequest {
stock: Some(product.stock - quantity),
..Default::default()
}).await?;
Ok(product)
})
.await
创建多个相关记录
Transaction::new(pool)
.with_orders()
.with_order_items()
.run(|mut ctx| async move {
// 创建父记录
let order = ctx.orders().create(CreateOrderRequest {
customer_id,
status: "pending".to_string(),
}).await?;
// 创建子记录
for item in items {
ctx.order_items().create(CreateOrderItemRequest {
order_id: order.id,
product_id: item.product_id,
quantity: item.quantity,
}).await?;
}
Ok(order)
})
.await
另请参阅
- [[属性|属性参考]] — 完整的属性文档
- [[钩子|生命周期钩子]] — 在操作前/后运行代码
- [[命令|命令]] — CQRS命令模式
- [[事件|事件]] — 跟踪实体变更
自定义SQL
当自动生成的SQL不够用时,使用 sql = "trait" 获得完全控制。
何时使用自定义SQL
- Join连接 — 单次查询获取关联实体
- CTE — 复杂递归或多步查询
- 全文搜索 — PostgreSQL
tsvector/tsquery - 聚合 —
GROUP BY、HAVING、窗口函数 - 分区表 — 基于时间或范围的分区
- 批量操作 — 批量插入/更新
- 软删除 — 自定义删除逻辑
- 乐观锁 — 基于版本的并发控制
基本配置
#[derive(Entity)]
#[entity(table = "posts", schema = "blog", sql = "trait")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub title: String,
#[field(create, response)]
pub author_id: Uuid,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
这会生成:
- 所有DTO(
CreatePostRequest、UpdatePostRequest、PostResponse) PostRow和InsertablePostPostRepositorytrait- 所有
From实现
但不会生成 impl PostRepository for PgPool。
实现Repository
use async_trait::async_trait;
use sqlx::PgPool;
#[async_trait]
impl PostRepository for PgPool {
type Error = sqlx::Error;
async fn create(&self, dto: CreatePostRequest) -> Result<Post, Self::Error> {
let entity = Post::from(dto);
let insertable = InsertablePost::from(&entity);
sqlx::query(
r#"
INSERT INTO blog.posts (id, title, author_id, created_at)
VALUES ($1, $2, $3, $4)
"#
)
.bind(insertable.id)
.bind(&insertable.title)
.bind(insertable.author_id)
.bind(insertable.created_at)
.execute(self)
.await?;
Ok(entity)
}
async fn find_by_id(&self, id: Uuid) -> Result<Option<Post>, Self::Error> {
let row: Option<PostRow> = sqlx::query_as(
"SELECT id, title, author_id, created_at FROM blog.posts WHERE id = $1"
)
.bind(&id)
.fetch_optional(self)
.await?;
Ok(row.map(Post::from))
}
async fn update(&self, id: Uuid, dto: UpdatePostRequest) -> Result<Post, Self::Error> {
// 你的自定义更新逻辑
todo!()
}
async fn delete(&self, id: Uuid) -> Result<bool, Self::Error> {
let result = sqlx::query("DELETE FROM blog.posts WHERE id = $1")
.bind(&id)
.execute(self)
.await?;
Ok(result.rows_affected() > 0)
}
async fn list(&self, limit: i64, offset: i64) -> Result<Vec<Post>, Self::Error> {
let rows: Vec<PostRow> = sqlx::query_as(
"SELECT id, title, author_id, created_at FROM blog.posts ORDER BY created_at DESC LIMIT $1 OFFSET $2"
)
.bind(limit)
.bind(offset)
.fetch_all(self)
.await?;
Ok(rows.into_iter().map(Post::from).collect())
}
}
示例:带作者Join的文章
// 带作者数据的扩展响应
pub struct PostWithAuthor {
pub post: Post,
pub author: User,
}
// 自定义repository扩展
pub trait PostRepositoryExt: PostRepository {
async fn find_with_author(&self, id: Uuid) -> Result<Option<PostWithAuthor>, Self::Error>;
async fn list_with_authors(&self, limit: i64, offset: i64) -> Result<Vec<PostWithAuthor>, Self::Error>;
}
#[async_trait]
impl PostRepositoryExt for PgPool {
async fn find_with_author(&self, id: Uuid) -> Result<Option<PostWithAuthor>, Self::Error> {
let row = sqlx::query_as::<_, (PostRow, UserRow)>(
r#"
SELECT
p.id, p.title, p.author_id, p.created_at,
u.id, u.username, u.email, u.created_at
FROM blog.posts p
JOIN auth.users u ON u.id = p.author_id
WHERE p.id = $1
"#
)
.bind(&id)
.fetch_optional(self)
.await?;
Ok(row.map(|(p, u)| PostWithAuthor {
post: Post::from(p),
author: User::from(u),
}))
}
async fn list_with_authors(&self, limit: i64, offset: i64) -> Result<Vec<PostWithAuthor>, Self::Error> {
// 类似的带join和分页的查询
todo!()
}
}
示例:全文搜索
pub trait PostSearchRepository {
async fn search(&self, query: &str, limit: i64) -> Result<Vec<Post>, sqlx::Error>;
}
#[async_trait]
impl PostSearchRepository for PgPool {
async fn search(&self, query: &str, limit: i64) -> Result<Vec<Post>, sqlx::Error> {
let rows: Vec<PostRow> = sqlx::query_as(
r#"
SELECT id, title, author_id, created_at
FROM blog.posts
WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', $1)
ORDER BY ts_rank(to_tsvector('english', title || ' ' || content), plainto_tsquery('english', $1)) DESC
LIMIT $2
"#
)
.bind(query)
.bind(limit)
.fetch_all(self)
.await?;
Ok(rows.into_iter().map(Post::from).collect())
}
}
示例:软删除
#[derive(Entity)]
#[entity(table = "posts", sql = "trait")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub title: String,
#[field(response)]
pub deleted_at: Option<DateTime<Utc>>,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
#[async_trait]
impl PostRepository for PgPool {
// ... 其他方法
async fn delete(&self, id: Uuid) -> Result<bool, Self::Error> {
// 软删除而不是硬删除
let result = sqlx::query(
"UPDATE blog.posts SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL"
)
.bind(&id)
.execute(self)
.await?;
Ok(result.rows_affected() > 0)
}
async fn list(&self, limit: i64, offset: i64) -> Result<Vec<Post>, Self::Error> {
// 排除已软删除的记录
let rows: Vec<PostRow> = sqlx::query_as(
r#"
SELECT id, title, deleted_at, created_at
FROM blog.posts
WHERE deleted_at IS NULL
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
"#
)
.bind(limit)
.bind(offset)
.fetch_all(self)
.await?;
Ok(rows.into_iter().map(Post::from).collect())
}
}
// 管理员的额外方法
pub trait PostAdminRepository {
async fn restore(&self, id: Uuid) -> Result<bool, sqlx::Error>;
async fn hard_delete(&self, id: Uuid) -> Result<bool, sqlx::Error>;
async fn list_deleted(&self, limit: i64, offset: i64) -> Result<Vec<Post>, sqlx::Error>;
}
示例:乐观锁
#[derive(Entity)]
#[entity(table = "documents", sql = "trait")]
pub struct Document {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub content: String,
#[field(response)]
pub version: i64,
#[auto]
#[field(response)]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug)]
pub enum DocumentError {
Sqlx(sqlx::Error),
ConcurrentModification,
}
#[async_trait]
impl DocumentRepository for PgPool {
type Error = DocumentError;
async fn update(&self, id: Uuid, dto: UpdateDocumentRequest) -> Result<Document, Self::Error> {
// 需要当前版本用于乐观锁
let expected_version = dto.version.ok_or(DocumentError::ConcurrentModification)?;
let row: Option<DocumentRow> = sqlx::query_as(
r#"
UPDATE documents
SET content = COALESCE($1, content),
version = version + 1,
updated_at = NOW()
WHERE id = $2 AND version = $3
RETURNING id, content, version, updated_at
"#
)
.bind(&dto.content)
.bind(&id)
.bind(expected_version)
.fetch_optional(self)
.await
.map_err(DocumentError::Sqlx)?;
row.map(Document::from)
.ok_or(DocumentError::ConcurrentModification)
}
// ... 其他方法
}
自定义SQL最佳实践
- 使用
query_as配合Row结构体 — 类型安全映射 - 绑定所有参数 — 永远不要插值字符串
- 返回Row,转换为Entity — 使用生成的
From实现 - 扩展,而不是替换 — 在
Repository旁边添加自定义trait - 使用真实数据库测试 — 集成测试是必不可少的
另见
- [[属性|属性]] — 完整属性参考
- [[案例|案例]] — 实际案例
- [[最佳实践|最佳实践]] — 性能技巧
Web框架
如何在流行的Rust Web框架中使用 entity-derive。
Axum
项目结构
src/
├── main.rs
├── entities/
│ ├── mod.rs
│ └── user.rs
├── handlers/
│ ├── mod.rs
│ └── users.rs
└── routes.rs
实体定义
// src/entities/user.rs
use entity_derive::Entity;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Entity, Clone)]
#[entity(table = "users", schema = "auth")]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub username: String,
#[field(create, update, response)]
pub email: String,
#[field(skip)]
pub password_hash: String,
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
}
处理器
// src/handlers/users.rs
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use sqlx::PgPool;
use uuid::Uuid;
use crate::entities::user::*;
pub async fn create_user(
State(pool): State<PgPool>,
Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserResponse>), StatusCode> {
let user = pool
.create(payload)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::CREATED, Json(UserResponse::from(&user))))
}
pub async fn get_user(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<UserResponse>, StatusCode> {
let user = pool
.find_by_id(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(UserResponse::from(&user)))
}
pub async fn update_user(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
Json(payload): Json<UpdateUserRequest>,
) -> Result<Json<UserResponse>, StatusCode> {
let user = pool
.update(id, payload)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(UserResponse::from(&user)))
}
pub async fn delete_user(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, StatusCode> {
let deleted = pool
.delete(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if deleted {
Ok(StatusCode::NO_CONTENT)
} else {
Err(StatusCode::NOT_FOUND)
}
}
pub async fn list_users(
State(pool): State<PgPool>,
) -> Result<Json<Vec<UserResponse>>, StatusCode> {
let users = pool
.list(100, 0)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let responses: Vec<UserResponse> = users.iter().map(UserResponse::from).collect();
Ok(Json(responses))
}
路由
// src/routes.rs
use axum::{
routing::{get, post, put, delete},
Router,
};
use sqlx::PgPool;
use crate::handlers::users;
pub fn create_router(pool: PgPool) -> Router {
Router::new()
.route("/users", post(users::create_user))
.route("/users", get(users::list_users))
.route("/users/:id", get(users::get_user))
.route("/users/:id", put(users::update_user))
.route("/users/:id", delete(users::delete_user))
.with_state(pool)
}
Main
// src/main.rs
use sqlx::postgres::PgPoolOptions;
use std::net::SocketAddr;
mod entities;
mod handlers;
mod routes;
#[tokio::main]
async fn main() {
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to create pool");
let app = routes::create_router(pool);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Actix Web
处理器
// src/handlers/users.rs
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use uuid::Uuid;
use crate::entities::user::*;
pub async fn create_user(
pool: web::Data<PgPool>,
payload: web::Json<CreateUserRequest>,
) -> impl Responder {
match pool.create(payload.into_inner()).await {
Ok(user) => HttpResponse::Created().json(UserResponse::from(&user)),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
pub async fn get_user(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
) -> impl Responder {
let id = path.into_inner();
match pool.find_by_id(id).await {
Ok(Some(user)) => HttpResponse::Ok().json(UserResponse::from(&user)),
Ok(None) => HttpResponse::NotFound().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
pub async fn update_user(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
payload: web::Json<UpdateUserRequest>,
) -> impl Responder {
let id = path.into_inner();
match pool.update(id, payload.into_inner()).await {
Ok(user) => HttpResponse::Ok().json(UserResponse::from(&user)),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
pub async fn delete_user(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
) -> impl Responder {
let id = path.into_inner();
match pool.delete(id).await {
Ok(true) => HttpResponse::NoContent().finish(),
Ok(false) => HttpResponse::NotFound().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
pub async fn list_users(pool: web::Data<PgPool>) -> impl Responder {
match pool.list(100, 0).await {
Ok(users) => {
let responses: Vec<UserResponse> = users.iter().map(UserResponse::from).collect();
HttpResponse::Ok().json(responses)
}
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
路由
// src/routes.rs
use actix_web::web;
use crate::handlers::users;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/users")
.route("", web::post().to(users::create_user))
.route("", web::get().to(users::list_users))
.route("/{id}", web::get().to(users::get_user))
.route("/{id}", web::put().to(users::update_user))
.route("/{id}", web::delete().to(users::delete_user)),
);
}
Main
// src/main.rs
use actix_web::{App, HttpServer, web};
use sqlx::postgres::PgPoolOptions;
mod entities;
mod handlers;
mod routes;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to create pool");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(pool.clone()))
.configure(routes::configure)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
错误处理
使用自定义类型进行更好的错误处理:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub enum AppError {
NotFound,
Database(sqlx::Error),
Validation(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "资源未找到"),
AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "数据库错误"),
AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
AppError::Database(err)
}
}
// 在处理器中使用:
pub async fn get_user(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<UserResponse>, AppError> {
let user = pool
.find_by_id(id)
.await?
.ok_or(AppError::NotFound)?;
Ok(Json(UserResponse::from(&user)))
}
验证
使用 validator crate 添加验证:
use validator::Validate;
// 手动为生成的结构体添加 Validate derive
// 或在调用repository方法之前进行验证
pub async fn create_user(
State(pool): State<PgPool>,
Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<UserResponse>), AppError> {
// 手动验证
if payload.username.len() < 3 {
return Err(AppError::Validation("用户名太短".into()));
}
if !payload.email.contains('@') {
return Err(AppError::Validation("无效的邮箱".into()));
}
let user = pool.create(payload).await?;
Ok((StatusCode::CREATED, Json(UserResponse::from(&user))))
}
分页
分页辅助示例:
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Pagination {
#[serde(default = "default_limit")]
pub limit: i64,
#[serde(default)]
pub offset: i64,
}
fn default_limit() -> i64 {
20
}
pub async fn list_users(
State(pool): State<PgPool>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Vec<UserResponse>>, AppError> {
let users = pool
.list(pagination.limit.min(100), pagination.offset)
.await?;
let responses: Vec<UserResponse> = users.iter().map(UserResponse::from).collect();
Ok(Json(responses))
}
另见
- [[案例|案例]] — 实际案例
- [[最佳实践|最佳实践]] — 生产指南
- [[属性|属性]] — 完整属性参考
最佳实践
在生产环境中有效使用 entity-derive 的指南。
实体设计
保持实体专注
一个实体对应一张数据库表。不要试图在单个实体中建模复杂的关系。
// 好:分离的实体
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
}
#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
#[id]
pub id: Uuid,
#[field(create, response)]
pub author_id: Uuid, // 引用,不是嵌入
#[field(create, update, response)]
pub title: String,
}
// 坏:试图嵌入关系
pub struct User {
pub id: Uuid,
pub posts: Vec<Post>, // 不要这样做
}
使用有意义的字段属性
明确每个字段的用途:
// 好:意图清晰
#[field(create, response)] // 设置一次,始终可见
pub email: String,
#[field(update, response)] // 可以更改,始终可见
pub display_name: Option<String>,
#[field(response)] // 只读,在其他地方计算/管理
pub post_count: i64,
#[field(skip)] // 从不暴露
pub password_hash: String,
// 坏:到处都是
#[field(create, update, response)] // 真的需要所有这些吗?
pub internal_id: String,
对可空字段使用 Option
与数据库模式匹配:
// 数据库:email VARCHAR NOT NULL
#[field(create, update, response)]
pub email: String,
// 数据库:bio TEXT NULL
#[field(update, response)]
pub bio: Option<String>,
安全性
始终对敏感数据使用 #[field(skip)]
// 密码
#[field(skip)]
pub password_hash: String,
// API密钥
#[field(skip)]
pub api_key: String,
// 内部令牌
#[field(skip)]
pub refresh_token: Option<String>,
// 不应出现在响应中的PII
#[field(skip)]
pub ssn: String,
// 内部审计数据
#[field(skip)]
pub created_by_ip: String,
分离内部和外部实体
对于仅限管理员的数据,考虑使用单独的实体:
// 公共实体
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(skip)]
pub admin_notes: Option<String>,
}
// 仅管理员实体(相同的表,不同的视图)
#[derive(Entity)]
#[entity(table = "users", sql = "trait")]
pub struct AdminUser {
#[id]
pub id: Uuid,
#[field(response)]
pub name: String,
#[field(update, response)] // 现在可见且可编辑
pub admin_notes: Option<String>,
#[field(response)]
pub last_login_ip: Option<String>,
}
性能
对复杂查询使用 sql = "trait"
不要与生成的SQL作斗争。如果需要join或复杂逻辑,自己实现:
// 简单CRUD - 使用完整生成
#[entity(table = "categories", sql = "full")]
// 需要复杂查询 - 自己实现
#[entity(table = "posts", sql = "trait")]
批量操作
对于批量插入,实现自定义方法:
#[entity(table = "events", sql = "trait")]
pub struct Event { /* ... */ }
pub trait EventBatchRepository {
async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error>;
}
#[async_trait]
impl EventBatchRepository for PgPool {
async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error> {
let mut tx = self.begin().await?;
for event in events {
let entity = Event::from(event);
let insertable = InsertableEvent::from(&entity);
// 在事务中插入
}
tx.commit().await?;
Ok(())
}
}
避免N+1查询
使用join而不是逐个加载相关实体:
// 坏:N+1查询
let posts = pool.list(100, 0).await?;
for post in &posts {
let author = pool.find_user_by_id(post.author_id).await?; // N次查询!
}
// 好:单次带join的查询
let posts_with_authors = pool.list_with_authors(100, 0).await?; // 1次查询
测试
使用单独的测试数据库
#[cfg(test)]
mod tests {
use sqlx::PgPool;
async fn setup_test_db() -> PgPool {
let url = std::env::var("TEST_DATABASE_URL")
.expect("TEST_DATABASE_URL must be set");
let pool = PgPool::connect(&url).await.unwrap();
// 运行迁移
sqlx::migrate!("./migrations")
.run(&pool)
.await
.unwrap();
pool
}
#[tokio::test]
async fn test_create_user() {
let pool = setup_test_db().await;
let request = CreateUserRequest {
username: "test_user".into(),
email: "test@example.com".into(),
};
let user = pool.create(request).await.unwrap();
assert_eq!(user.username, "test_user");
}
}
单独测试DTO
#[test]
fn test_user_response_excludes_password() {
let user = User {
id: Uuid::new_v4(),
username: "test".into(),
email: "test@example.com".into(),
password_hash: "secret_hash".into(),
created_at: Utc::now(),
};
let response = UserResponse::from(&user);
// password_hash 不在 UserResponse 中
assert_eq!(response.username, "test");
// 无法通过 response 访问 password_hash
}
#[test]
fn test_update_request_is_partial() {
let update = UpdateUserRequest {
username: Some("new_name".into()),
email: None, // 不更新email
};
assert!(update.username.is_some());
assert!(update.email.is_none());
}
项目组织
推荐结构
src/
├── entities/ # 实体定义
│ ├── mod.rs
│ ├── user.rs
│ ├── post.rs
│ └── comment.rs
├── repositories/ # 自定义repository扩展
│ ├── mod.rs
│ └── post_search.rs
├── handlers/ # HTTP处理器
│ ├── mod.rs
│ ├── users.rs
│ └── posts.rs
├── services/ # 业务逻辑
│ ├── mod.rs
│ └── auth.rs
└── main.rs
重新导出生成的类型
// src/entities/mod.rs
mod user;
mod post;
pub use user::*;
pub use post::*;
分组相关实体
// src/entities/auth/mod.rs
mod user;
mod session;
mod api_key;
pub use user::*;
pub use session::*;
pub use api_key::*;
常见错误
1. 忘记在敏感字段上使用 #[field(skip)]
// 错误:password_hash 会出现在 Response 中!
pub struct User {
pub password_hash: String,
}
// 正确
#[field(skip)]
pub password_hash: String,
2. 当需要Join时使用 sql = "full"
如果需要关联数据,使用 sql = "trait" 并自己实现。
3. 不处理可选更新
记住:UpdateRequest 字段是 Option<T>。应用前先检查:
// 生成的 UpdateUserRequest 的 name 字段是 Option<String>
// 你的更新逻辑应该处理 None(不更改)vs Some(更改)
4. 重复业务逻辑
将验证和业务规则放在服务层,而不是handlers中:
// 好:服务层
impl UserService {
pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, AppError> {
self.validate_email(&request.email)?;
self.check_username_available(&request.username).await?;
self.pool.create(request).await.map_err(Into::into)
}
}
// 坏:逻辑分散在handlers中
pub async fn create_user(pool: State<PgPool>, request: Json<CreateUserRequest>) -> ... {
// 验证在这里
// 业务规则在这里
// Repository调用在这里
// 全部混在一起
}
检查清单
部署前:
- 所有敏感字段都有
#[field(skip)] - DTO符合API契约预期
- 复杂查询使用
sql = "trait" - 集成测试覆盖repository方法
- 错误处理一致
- 列表端点实现了分页
- 查询模式有数据库索引
另见
- [[属性|属性]] — 完整属性参考
- [[案例|案例]] — 实际案例
- [[Web框架|Web框架]] — 框架集成