Guías para usar entity-derive efectivamente en producción.
Diseño de Entidades
Mantén las Entidades Enfocadas
Una entidad por tabla de base de datos. No intentes modelar relaciones complejas en una sola entidad.
// Bueno: Entidades separadas
#[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, // Referencia, no embeber
#[field(create, update, response)]
pub title: String,
}
// Malo: Intentar embeber relaciones
pub struct User {
pub id: Uuid,
pub posts: Vec<Post>, // No hagas esto
}
Usa Atributos de Campo Significativos
Sé explícito sobre el propósito de cada campo:
// Bueno: Intención clara
#[field(create, response)] // Se establece una vez, siempre visible
pub email: String,
#[field(update, response)] // Puede cambiar, siempre visible
pub display_name: Option<String>,
#[field(response)] // Solo lectura, calculado/gestionado externamente
pub post_count: i64,
#[field(skip)] // Nunca expuesto
pub password_hash: String,
// Malo: Todo en todas partes
#[field(create, update, response)] // ¿Es realmente necesario para todo?
pub internal_id: String,
Prefiere Option para Campos Nulables
Haz coincidir con tu esquema de base de datos:
// Base de datos: email VARCHAR NOT NULL
#[field(create, update, response)]
pub email: String,
// Base de datos: bio TEXT NULL
#[field(update, response)]
pub bio: Option<String>,
Seguridad
Siempre Usa #[field(skip)] para Datos Sensibles
// Contraseñas
#[field(skip)]
pub password_hash: String,
// Claves API
#[field(skip)]
pub api_key: String,
// Tokens internos
#[field(skip)]
pub refresh_token: Option<String>,
// PII que no debería estar en respuestas
#[field(skip)]
pub ssn: String,
// Datos de auditoría interna
#[field(skip)]
pub created_by_ip: String,
Separa Entidades Internas y Externas
Para datos solo de administrador, considera entidades separadas:
// Entidad pública
#[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>,
}
// Entidad solo admin (misma tabla, vista diferente)
#[derive(Entity)]
#[entity(table = "users", sql = "trait")]
pub struct AdminUser {
#[id]
pub id: Uuid,
#[field(response)]
pub name: String,
#[field(update, response)] // Ahora visible y editable
pub admin_notes: Option<String>,
#[field(response)]
pub last_login_ip: Option<String>,
}
Rendimiento
Usa sql = "trait" para Consultas Complejas
No luches contra el SQL generado. Si necesitas joins o lógica compleja, impleméntalo tú mismo:
// CRUD simple - usa generación completa
#[entity(table = "categories", sql = "full")]
// Consultas complejas necesarias - implementa tú mismo
#[entity(table = "posts", sql = "trait")]
Operaciones por Lotes
Para inserciones masivas, implementa métodos personalizados:
#[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);
// Insertar dentro de transacción
}
tx.commit().await?;
Ok(())
}
}
Evita Consultas N+1
Usa joins en lugar de cargar entidades relacionadas una por una:
// Malo: Consultas 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 consultas!
}
// Bueno: Una sola consulta con join
let posts_with_authors = pool.list_with_authors(100, 0).await?; // 1 consulta
Testing
Usa Base de Datos de Prueba Separada
#[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();
// Ejecutar migraciones
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");
}
}
Prueba DTOs Separadamente
#[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 no está en UserResponse
assert_eq!(response.username, "test");
// No hay forma de acceder a password_hash a través de response
}
#[test]
fn test_update_request_is_partial() {
let update = UpdateUserRequest {
username: Some("new_name".into()),
email: None, // No actualizando email
};
assert!(update.username.is_some());
assert!(update.email.is_none());
}
Organización del Proyecto
Estructura Recomendada
src/
├── entities/ # Definiciones de entidades
│ ├── mod.rs
│ ├── user.rs
│ ├── post.rs
│ └── comment.rs
├── repositories/ # Extensiones de repository personalizadas
│ ├── mod.rs
│ └── post_search.rs
├── handlers/ # Handlers HTTP
│ ├── mod.rs
│ ├── users.rs
│ └── posts.rs
├── services/ # Lógica de negocio
│ ├── mod.rs
│ └── auth.rs
└── main.rs
Re-exporta Tipos Generados
// src/entities/mod.rs
mod user;
mod post;
pub use user::*;
pub use post::*;
Agrupa Entidades Relacionadas
// src/entities/auth/mod.rs
mod user;
mod session;
mod api_key;
pub use user::*;
pub use session::*;
pub use api_key::*;
Errores Comunes
1. Olvidar #[field(skip)] en Campos Sensibles
// Incorrecto: password_hash estará en Response!
pub struct User {
pub password_hash: String,
}
// Correcto
#[field(skip)]
pub password_hash: String,
2. Usar sql = "full" Cuando Necesitas Joins
Si necesitas datos relacionados, usa sql = "trait" e implementa tú mismo.
3. No Manejar Actualizaciones Opcionales
Recuerda: los campos de UpdateRequest son Option<T>. Verifica antes de aplicar:
// UpdateUserRequest generado tiene Option<String> para name
// Tu lógica de actualización debería manejar None (sin cambio) vs Some (cambio)
4. Duplicar Lógica de Negocio
Pon validación y reglas de negocio en una capa de servicio, no en handlers:
// Bueno: Capa de servicio
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)
}
}
// Malo: Lógica dispersa en handlers
pub async fn create_user(pool: State<PgPool>, request: Json<CreateUserRequest>) -> ... {
// Validación aquí
// Reglas de negocio aquí
// Llamada al repository aquí
// Todo mezclado
}
Lista de Verificación
Antes de desplegar:
- Todos los campos sensibles tienen
#[field(skip)] - Los DTOs coinciden con las expectativas del contrato API
- Las consultas complejas usan
sql = "trait" - Los tests de integración cubren métodos del repository
- El manejo de errores es consistente
- La paginación está implementada para endpoints de lista
- Existen índices de base de datos para patrones de consulta
Ver También
- [[Atributos|Atributos]] — Referencia completa de atributos
- [[Ejemplos|Ejemplos]] — Ejemplos del mundo real
- [[Frameworks Web|Frameworks-Web]] — Integración con frameworks