-
-
Notifications
You must be signed in to change notification settings - Fork 0
SQL Personalizado
RAprogramm edited this page Jan 7, 2026
·
2 revisions
Cuando el SQL generado automáticamente no es suficiente, usa sql = "trait" para control total.
- Joins — Entidades relacionadas en una sola consulta
- CTEs — Consultas recursivas o de múltiples pasos
-
Búsqueda full-text — PostgreSQL
tsvector/tsquery -
Agregaciones —
GROUP BY,HAVING, funciones de ventana - Tablas particionadas — Particiones basadas en tiempo o rango
- Operaciones bulk — Inserciones/actualizaciones por lotes
- Borrado lógico — Lógica de eliminación personalizada
- Bloqueo optimista — Concurrencia basada en versión
#[derive(Entity)] #[entity(table = "posts", schema = "blog", sql = "trait")] pub struct Post { #[id] pub id: Uuid, #[field(create, update, response)] pub title: String, #[field(create, response)] pub author_id: Uuid, #[auto] #[field(response)] pub created_at: DateTime<Utc>, }
Esto genera:
- Todos los DTOs (
CreatePostRequest,UpdatePostRequest,PostResponse) -
PostRoweInsertablePost - Trait
PostRepository - Todas las implementaciones
From
Pero no genera impl PostRepository for PgPool.
use async_trait::async_trait; use sqlx::PgPool; #[async_trait] impl PostRepository for PgPool { type Error = sqlx::Error; async fn create(&self, dto: CreatePostRequest) -> Result<Post, Self::Error> { let entity = Post::from(dto); let insertable = InsertablePost::from(&entity); sqlx::query( r#" INSERT INTO blog.posts (id, title, author_id, created_at) VALUES (1,ドル 2,ドル 3,ドル 4ドル) "# ) .bind(insertable.id) .bind(&insertable.title) .bind(insertable.author_id) .bind(insertable.created_at) .execute(self) .await?; Ok(entity) } async fn find_by_id(&self, id: Uuid) -> Result<Option<Post>, Self::Error> { let row: Option<PostRow> = sqlx::query_as( "SELECT id, title, author_id, created_at FROM blog.posts WHERE id = 1ドル" ) .bind(&id) .fetch_optional(self) .await?; Ok(row.map(Post::from)) } async fn update(&self, id: Uuid, dto: UpdatePostRequest) -> Result<Post, Self::Error> { // Tu lógica de actualización personalizada todo!() } async fn delete(&self, id: Uuid) -> Result<bool, Self::Error> { let result = sqlx::query("DELETE FROM blog.posts WHERE id = 1ドル") .bind(&id) .execute(self) .await?; Ok(result.rows_affected() > 0) } async fn list(&self, limit: i64, offset: i64) -> Result<Vec<Post>, Self::Error> { let rows: Vec<PostRow> = sqlx::query_as( "SELECT id, title, author_id, created_at FROM blog.posts ORDER BY created_at DESC LIMIT 1ドル OFFSET 2ドル" ) .bind(limit) .bind(offset) .fetch_all(self) .await?; Ok(rows.into_iter().map(Post::from).collect()) } }
// Respuesta extendida con datos del autor pub struct PostWithAuthor { pub post: Post, pub author: User, } // Extensión de repository personalizada pub trait PostRepositoryExt: PostRepository { async fn find_with_author(&self, id: Uuid) -> Result<Option<PostWithAuthor>, Self::Error>; async fn list_with_authors(&self, limit: i64, offset: i64) -> Result<Vec<PostWithAuthor>, Self::Error>; } #[async_trait] impl PostRepositoryExt for PgPool { async fn find_with_author(&self, id: Uuid) -> Result<Option<PostWithAuthor>, Self::Error> { let row = sqlx::query_as::<_, (PostRow, UserRow)>( r#" SELECT p.id, p.title, p.author_id, p.created_at, u.id, u.username, u.email, u.created_at FROM blog.posts p JOIN auth.users u ON u.id = p.author_id WHERE p.id = 1ドル "# ) .bind(&id) .fetch_optional(self) .await?; Ok(row.map(|(p, u)| PostWithAuthor { post: Post::from(p), author: User::from(u), })) } async fn list_with_authors(&self, limit: i64, offset: i64) -> Result<Vec<PostWithAuthor>, Self::Error> { // Consulta similar con join y paginación todo!() } }
pub trait PostSearchRepository { async fn search(&self, query: &str, limit: i64) -> Result<Vec<Post>, sqlx::Error>; } #[async_trait] impl PostSearchRepository for PgPool { async fn search(&self, query: &str, limit: i64) -> Result<Vec<Post>, sqlx::Error> { let rows: Vec<PostRow> = sqlx::query_as( r#" SELECT id, title, author_id, created_at FROM blog.posts WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', 1ドル) ORDER BY ts_rank(to_tsvector('english', title || ' ' || content), plainto_tsquery('english', 1ドル)) DESC LIMIT 2ドル "# ) .bind(query) .bind(limit) .fetch_all(self) .await?; Ok(rows.into_iter().map(Post::from).collect()) } }
#[derive(Entity)] #[entity(table = "posts", sql = "trait")] pub struct Post { #[id] pub id: Uuid, #[field(create, update, response)] pub title: String, #[field(response)] pub deleted_at: Option<DateTime<Utc>>, #[auto] #[field(response)] pub created_at: DateTime<Utc>, } #[async_trait] impl PostRepository for PgPool { // ... otros métodos async fn delete(&self, id: Uuid) -> Result<bool, Self::Error> { // Borrado lógico en lugar de borrado físico let result = sqlx::query( "UPDATE blog.posts SET deleted_at = NOW() WHERE id = 1ドル AND deleted_at IS NULL" ) .bind(&id) .execute(self) .await?; Ok(result.rows_affected() > 0) } async fn list(&self, limit: i64, offset: i64) -> Result<Vec<Post>, Self::Error> { // Excluir borrados lógicos let rows: Vec<PostRow> = sqlx::query_as( r#" SELECT id, title, deleted_at, created_at FROM blog.posts WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 1ドル OFFSET 2ドル "# ) .bind(limit) .bind(offset) .fetch_all(self) .await?; Ok(rows.into_iter().map(Post::from).collect()) } } // Método adicional para administradores pub trait PostAdminRepository { async fn restore(&self, id: Uuid) -> Result<bool, sqlx::Error>; async fn hard_delete(&self, id: Uuid) -> Result<bool, sqlx::Error>; async fn list_deleted(&self, limit: i64, offset: i64) -> Result<Vec<Post>, sqlx::Error>; }
#[derive(Entity)] #[entity(table = "documents", sql = "trait")] pub struct Document { #[id] pub id: Uuid, #[field(create, update, response)] pub content: String, #[field(response)] pub version: i64, #[auto] #[field(response)] pub updated_at: DateTime<Utc>, } #[derive(Debug)] pub enum DocumentError { Sqlx(sqlx::Error), ConcurrentModification, } #[async_trait] impl DocumentRepository for PgPool { type Error = DocumentError; async fn update(&self, id: Uuid, dto: UpdateDocumentRequest) -> Result<Document, Self::Error> { // Requiere versión actual para bloqueo optimista let expected_version = dto.version.ok_or(DocumentError::ConcurrentModification)?; let row: Option<DocumentRow> = sqlx::query_as( r#" UPDATE documents SET content = COALESCE(1,ドル content), version = version + 1, updated_at = NOW() WHERE id = 2ドル AND version = 3ドル RETURNING id, content, version, updated_at "# ) .bind(&dto.content) .bind(&id) .bind(expected_version) .fetch_optional(self) .await .map_err(DocumentError::Sqlx)?; row.map(Document::from) .ok_or(DocumentError::ConcurrentModification) } // ... otros métodos }
-
Usa
query_ascon estructuras Row — Mapeo type-safe - Vincula todos los parámetros — Nunca interpoles strings
-
Retorna Row, convierte a Entity — Usa las impl
Fromgeneradas -
Extiende, no reemplaces — Añade traits personalizados junto a
Repository - Prueba con base de datos real — Los tests de integración son esenciales
- Atributos — Referencia completa de atributos
- Ejemplos — Ejemplos del mundo real
- Mejores Prácticas — Consejos de rendimiento
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级