Skip to content
RAprogramm edited this page Jan 7, 2026 · 3 revisions

entity-derive

一个宏统治一切

English Русский 한국어 Español 中文


这是什么?

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行 → 完整的领域层:

  • CreateUserRequestUpdateUserRequestUserResponse
  • 类型安全SQL的 UserRepository
  • UserEvent::CreatedUpdatedDeleted
  • 业务逻辑的 UserHooks
  • RegisterUserDeactivateUser 命令
  • 用于过滤的 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,
}

完成。 你现在拥有:

  • CreatePostRequestUpdatePostRequestPostResponse
  • 带有 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 — 命令名称 = 业务词汇
  • 不同逻辑DeactivateBan 可以有不同的副作用
  • 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?启用 commandsevents。架构随项目成长。

可扩展性

级别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_tohas_many
事件 生命周期事件
钩子 Before/after钩子
命令 CQRS模式
自定义SQL 复杂查询
示例 真实用例
Web框架 Axum、Actix集成
最佳实践 生产指南

这不是一个规定你如何生活的框架。 这是一个消除例行工作让你正确构建的工具。


Clone this wiki locally