-
-
Notifications
You must be signed in to change notification settings - Fork 0
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"零魔法。 如果宏坏了 — 你总是可以手写代码。没有锁定。
#[field(skip)]
pub password_hash: String,这不是运行时检查"不要序列化这个字段"。这是字段在 UserResponse 结构中物理上不存在。不可能意外返回 — 字段根本不存在。
生成的代码是:
- 没有Box/dyn的普通
struct - 直接调用sqlx,没有中间层
- 热路径上的
#[inline] - 没有超出必要的分配
基准测试: 生成的repository运行速度与手写相同。因为它就是相同的代码。
// 一切都是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>如果代码编译了 — 它就能正确运行。
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。这些都是实现细节。
// 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 | 复杂查询 |
| 示例 | 真实用例 |
| Web框架 | Axum、Actix集成 |
| 最佳实践 | 生产指南 |
这不是一个规定你如何生活的框架。 这是一个消除例行工作让你正确构建的工具。
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级