Skip to content

最佳实践

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: "[email protected]".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: "[email protected]".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方法
  • 错误处理一致
  • 列表端点实现了分页
  • 查询模式有数据库索引

另见

Clone this wiki locally