Skip to content

Navigation Menu

Sign in
Sign up

最佳实践

RAprogramm edited this page Jan 7, 2026 · 2 revisions

在生产环境中有效使用 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方法
  • 错误处理一致
  • 列表端点实现了分页
  • 查询模式有数据库索引

另见

🌐 Language

🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文


🇬🇧 English

Home

Getting Started

Features

Advanced


🇷🇺 Русский

Главная

Начало работы

Возможности

Продвинутое


🇰🇷 한국어

시작하기

기능

고급


🇪🇸 Español

Inicio

Comenzando

Características

Avanzado


🇨🇳 中文

首页

入门

功能

高级


📚 Reference

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /