Skip to content

Navigation Menu

Sign in
Sign up

Mejores Prácticas

RAprogramm edited this page Jan 7, 2026 · 2 revisions

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

🌐 Language

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


🇬🇧 English

Home

Getting Started

Features

Advanced


🇷🇺 Русский

Главная

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

Возможности

Продвинутое


🇰🇷 한국어

시작하기

기능

고급


🇪🇸 Español

Inicio

Comenzando

Características

Avanzado


🇨🇳 中文

首页

入门

功能

高级


📚 Reference

Clone this wiki locally

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