Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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_deletefalse启用软删除
returning"full"RETURNING子句模式
upsert(...)生成基于 INSERT ... ON CONFLICT 的 upsert 方法
api(guard = "...")在生成的 handler 中强制执行的 FromRequestParts 守卫
errorsqlx::Error自定义错误类型
eventsfalse生成生命周期事件
hooksfalse生成生命周期钩子trait
commandsfalse启用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 TraitPgPool实现用例
"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() 而不是 DELETE
  • hard_delete() — 永久删除记录
  • restore() — 设置 deleted_at = NULL
  • find_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 CONFLICTupsert 仓库方法。

#[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", ...),操作包括 creategetupdatedeletelistcommands;字面量 "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属性配置)
  • 始终包含在 Response DTO中
  • CreateRequestUpdateRequest 中排除
#[id]
pub id: Uuid,

#[auto]

标记自动生成的字段(时间戳、序列)。

行为:

  • From<CreateRequest> 中获取 Default::default()
  • CreateRequestUpdateRequest 中排除
  • 可通过 #[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_TYPE DDL 注册到 {Entity}::MIGRATION_TYPES — 请在 MIGRATION_UP 之前执行
  • 声明的名称在编译期与枚举的 PG_TYPE 常量核对,不一致会导致构建失败
  • ValueObject 的可选 sqlx 标志会生成 sqlx::Type / Encode / Decode 实现;若已自行 derive sqlx::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_scopedlist_by_ownerupdate_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"
使用时间排序UUIDuuid = "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,无DBsql = "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 使用双重 OptionNone = 保留,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 字段,编译期校验)、auditentity_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(...)] 规则(lengthrangeemailurlpattern)会翻译为 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。类型:uniqueforeign_keycheck。自定义条目优先于同名的派生条目。需要 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

同时启用 transactionsupsert(...) 时,{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?;