-
-
Notifications
You must be signed in to change notification settings - Fork 0
最佳实践
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,与数据库模式匹配:
// 数据库:email VARCHAR NOT NULL
#[field(create, update, response)]
pub email: String,
// 数据库:bio TEXT NULL
#[field(update, response)]
pub bio: Option<String>,// 密码
#[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作斗争。如果需要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(())
}
}使用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");
}
}#[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::*;// 错误:password_hash 会出现在 Response 中!
pub struct User {
pub password_hash: String,
}
// 正确
#[field(skip)]
pub password_hash: String,如果需要关联数据,使用 sql = "trait" 并自己实现。
记住:UpdateRequest 字段是 Option<T>。应用前先检查:
// 生成的 UpdateUserRequest 的 name 字段是 Option<String>
// 你的更新逻辑应该处理 None(不更改)vs Some(更改)将验证和业务规则放在服务层,而不是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方法
- 错误处理一致
- 列表端点实现了分页
- 查询模式有数据库索引
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级