-
-
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: "test@example.com".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: "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::*;
// 错误: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 | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级