Notas
- SQLx no es un ORM
- Compatible con PostgreSQL , MySQL , MariaDB y SQLite
- Consultas verificadas en tiempo de compilación con las macros como
query!yquery_as! sqlx::queryprepara automáticamente las sentencias SQL y las guarda en caché para cada conexión a la base de datos- Ejecución de consultas simples (sin preparación)
- El controlador
Anypermite adaptarse en tiempo de ejecución.AnyPoolse conecta al controlador indicado por el esquema de la URL.
Resumen
Query
query: Ejecuta consultas genéricas, devuelve filas que se pueden manejar manualmentequery_as: Mapea el resultado de la consulta a un struct específicoquery!: Valida la consulta y los nombres de las columnas en tiempo de compilación, devolviendo filas con acceso por nombre o posiciónquery_as!: Valida la consulta en tiempo de compilación y mapea los resultados a un struct específico
Tipo de solicitudes
executedevuelve las filas afectadas (affected_rows) y el resto lo omitefetchdevuelve un stream de datos (filas)fetch_onedevuelve una fila o errorfetch_optionaldevuelve una fila o Nonefetch_alldevuelve todas las filas
Consultas
Consultas preparadas (parametrizadas)
- Las consultas preparadas almacenan su plan de consulta en caché (cachea la consulta no los resultados)
- Utilizan un modo de comunicación binario (menor ancho de banda y decodificación más rápida)
QueryyQueryAsse consideran consultas preparadas- Utilizan parámetros ($1, $2, etc.) para evitar la inyección de SQL y se agregan con
.bind(value) - Para MySQL/MariaDB se utiliza un signo de interrogación
?en lugar de$1
// prepared, cached query
conn.execute(sqlx::query("DELETE FROM table")).await?;
- Para evitar el encapsulamiento se puede ejecutar
queryfuera deexecute
sqlx::query("DELETE FROM table").execute(&mut conn).await?;
sqlx::query("DELETE FROM table").execute(&pool).await?;
Consultas no preparadas (simples)
&strse considera una consulta no preparada- Esto puede ser más rápido para consultas que se ejecutan solo una vez o que no necesitan la flexibilidad de los parámetros
conn.execute("BEGIN").await?; // unprepared, simple query
query y query_as
- Los parámetros se agregan con
.bind(value)
query
- Mapeando manualmente la respuesta
- Agregar
use sqlx::Row;para poder acceder a los valores - Se accede a los valores por índice o por nombre de las columnas, utilizando
row.get(...) - Se debe especificar el tipo que se está extrayendo
use sqlx::Row; // Necessary
let book = sqlx::query("SELECT * FROM books WHERE id = $1;")
.bind(uuid)
.fetch_one(&db_pool)
.await
.unwrap();
println!("{:?}", book);
// Ejemplo 1.
let book_id: Uuid = book.get("id");
let book_title: String = book.get(1);
println!("ID - {} - Title: {}", book_id, book_title);
// Ejemplo 2. Mapeando a un struct
// Se debee especificar el tipo
// Se accede con el nombre de la columna o el indice
let book = Book {
id: row.get("id"),
title: row.get("title"),
};
// Ejemplo 3. Mapeando a un Vec<Book>
let books: Vec<Book> = rows
.into_iter()
.map(|row| {
Book {
id: row.get("id"),
title: row.get("title"),
}
})
.collect();
let maybe_book = maybe_row.map(|row| Book {
isbn: row.get("isbn"),
title: row.get("title"),
price: row.get("price"),
author_id: row.get("author_id"),
});
// Ejemplo 4
let mut stream = sqlx::query("SELECT * FROM users")
.map(|row: PgRow| {
// map the row into a user-defined domain type
})
.fetch(&mut conn);
query_as
- Para mapear automáticamente la respuesta a un struct
- Necesita el trait
#[derive(sqlx::FromRow)]sobre el struct
#[derive(FromRow)]
struct Book {
id: Uuid,
title: String,
}
let query = "SELECT id, title FROM books WHERE id = $1;";
let book = sqlx::query_as::<_, Book>(query)
.bind(uuid)
.fetch_optional(&db_pool)
.await
.unwrap();
#[derive(Serialize, Deserialize, FromRow)]
struct User {
id: i32,
name: String,
age: i32
}
#[derive(Serialize, Deserialize)]
struct UpdateUser {
name: Option<String>,
age: Option<i32>
}
async fn update_user(
State(pool): State<PgPool>,
Path(id): Path<i32>,
Json(payload): Json<UpdateUser>
) -> (StatusCode, Json<Value>) {
// Check if user exist
let res = sqlx::query_as::<_, User>(
"SELECT * FROM users WHERE id = $1"
).bind(id)
.fetch_optional(&pool)
.await;
let _exist_user = match res {
Ok(Some(user)) => user,
Ok(None) => return (StatusCode::NOT_FOUND, Json(json!({"message": "Not found"}))),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"message": e.to_string()})))
};
// e.g PUT method (complete update)
//let query_result = sqlx::query(
// "UPDATE users SET name = $1, age = $2 WHERE id = $3;"
// )
// .bind(payload.name)
// .bind(payload.age)
// .bind(id)
// .execute(&pool)
// .await;
// e.g. PATCH method (partial update, construct query)
// SET id = $1 initialize to avoid the possible error of no data coming in the body
let mut query = "UPDATE users SET id = $1".to_owned();
// For bindings $1, $2, $3, etc.
// Start with 2 because 1 is id = $1
let mut i = 2;
// Build query
if payload.name.is_some() {
query.push_str(&format!(", name = ${}", i));
i += 1;
}
if payload.age.is_some() {
query.push_str(&format!(", age = ${}", i));
}
// End of query
query.push_str(&format!(" WHERE id = $1;"));
// Bindings
let mut query = sqlx::query(&query).bind(id);
if payload.name.is_some(){
query = query.bind(payload.name)
}
if payload.age.is_some(){
query = query.bind(payload.age);
}
// Execute query
let query_result = query.execute(&pool).await;
match query_result {
Ok(result) if result.rows_affected() == 1 => {
(StatusCode::OK, Json(json!({ "message": "Updated" })))
},
Ok(_) => (StatusCode::NOT_FOUND, Json(json!({ "message": "Not found" }))),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "message": e.to_string() })),
)
}
}
query! y query_as!
- La macro
sqlx::query!ysqlx::query_as!permite una verificación sintáctica y semántica en tiempo de compilación y de desarrollo del SQL - Debe estar definida la variable
DATABASE_URLpara que funcione correctamente - Puede ser más lento al momento de desarrollo y construcción ya que siempre está haciendo comprobaciones
- Pueden surgir problemas al hacer el build con docker o en la nube ya que necesita la variable
DATABASE_URLpara poder compilar sin errores
Cargo.toml
[profile.dev.package.sqlx-macros]
opt-level = 3
query!
- Tiene una salida de un tipo anónimo donde cada columna SQL es un campo disponible
- Este tipo es un "registro anónimo", lo que significa que se genera dinámicamente en función de las columnas, pero no tiene un nombre de tipo (struct o tipo) explícito en Rust
- Se puede acceder a los valores usando los nombre de las columnas
- Sintaxis:
query!(query, param1, param2)
let result = sqlx::query!(
"SELECT id, title FROM books WHERE id = $1",
uuid!("abc")
)
.fetch_one(&db_pool)
.await;
match result {
Ok(book) => println!("Book ID: {}, Title: {}", book.id, book.title),
Err(e) => eprintln!("Error fetching book: {:?}", e),
}
query_as!
- Mapea el resultado de la consulta directamente a un struct
- Sintaxis:
query_as!(struct, query, param1, param2) - No es necesario definir traits sobre la estructura como se haría con
query_as(#[derive(sqlx::FromRow)])
// no traits are needed
struct Book {
id: Uuid,
title: String
}
let books = sqlx::query_as!(Book, "SELECT * FROM books")
.fetch_all(&db_pool)
.await
.unwrap();
println!(
"Book ID: {} - Title: {}",
books[0].id,
books[0].title
);
async fn get_user(State(pool): State<PgPool>) -> Result<Json<Value>, (StatusCode, String)> {
let rows = sqlx::query_as!(User, "SELECT * FROM users ORDER BY name")
.fetch_all(&pool)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
json!({"message": e.to_string()}).to_string(),
)
})?;
Ok(Json(json!({"total" : rows.len(), "data": rows})))
}
#[derive(Serialize, Deserialize)]
struct User {
id: i32,
name: String,
age: Option<i32>,
}
Otros query
Transacciones
- Una transacción comienza con una llamada a
Pool::beginoConnection::begin. - Una transacción debe finalizar con una llamada a
commitorollback. Si no se llama a ninguno antes de que la transacción quede fuera de alcance, se llama arollback. - Una transacción se puede utilizar como
Executoral realizar consultas
async fn create_author_with_book(
author: &Author,
book: &Book,
pool: &PgPool,
) -> Result<(), Box<dyn std::error::Error>> {
let mut tx = pool.begin().await?;
// Save author
let create_author_query = "INSERT INTO author (name) VALUES ($1) RETURNING id";
let author_id: (i32,) = sqlx::query_as(create_author_query)
.bind(&author.name)
.fetch_one(tx.acquire().await?)
//.execute(&mut *tx)
.await?;
// Save book
let create_book_query =
"INSERT INTO book (isbn, title, price, author_id) VALUES ($1, $2, $3, $4)";
sqlx::query(create_book_query)
.bind(&book.isbn)
.bind(&book.title)
.bind(&book.price)
.bind(author_id.0)
.execute(tx.acquire().await?)
//.execute(&mut *tx)
.await?;
txn.commit().await?;
Ok(())
}
query_scalar
- Una única consulta SQL como una declaración preparada que extrae solo la primera columna de cada fila
- Se utiliza para obtener un valor único (un escalar) de la consulta, en lugar de una fila o estructura completa
- Cuando se necesita extraer un solo valor de una consulta, como un conteo o un solo campo
// Obtener el conteo de usuarios en la tabla let
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
println!("Total de usuarios: {}", count);
query_scalar!
- Similar a
query_scalar, pero ofrece verificación en tiempo de compilación - Se utiliza cuando se quiere garantizar en tiempo de compilación que la consulta devolverá un valor escalar válido
// Obtener el conteo de usuarios en la tabla
// con verificación en tiempo de compilación
let count = query_scalar!("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
println!("Total de usuarios: {}", count);
query_as_with
- Cuando se necesita una transformación personalizada de las filas devueltas
- Es útil cuando se necesita transformar los datos antes de mapearlo
- Similar a
query_as, pero permite usar un tipo de conversión personalizada que implementasqlx::FromRowpara mapear los resultados de la consulta a una estructura
use sqlx::{query_as_with, FromRow, postgres::PgRow};
use std::convert::TryFrom;
#[derive(Debug)]
struct User {
id: i32,
name: String,
}
impl TryFrom<PgRow> for User {
type Error = sqlx::Error;
fn try_from(row: PgRow) -> Result<Self, Self::Error> {
Ok(User {
id: row.try_get("id")?,
// Convertir el nombre a mayúsculas
name: row.try_get("name")?.to_uppercase(),
})
}
}
// Mapeo personalizado usando query_as_with
let user = query_as_with(
"SELECT id, name FROM users WHERE email = $1",
"[email protected]"
)
.fetch_one(&pool)
.await
.unwrap();
println!("{:?}", user);
Finalizador de consultas
execute
- Devuelve el número de filas afectadas y descarta el resto si hay
- Útil cuando solo importa si la consulta fue exitosa y cuántas filas fueron afectadas
let result = sqlx::query("UPDATE books SET title = $1 WHERE id = $2")
.bind("Nuevo Título")
.bind(uuid)
.execute(&db_pool)
.await
.unwrap();
println!("Filas afectadas: {}", result.rows_affected());
fetch_one
- Devuelve un solo resultado (o fila)
- Marcará error si no devuelve un valor
fetch_optional
- Devuelve opcionalmente un solo valor
- No marca error si no encuentra resultados
- (Some, None)
fetch_all
- Devuelve todos los resultados de la consulta como un vector
fetch
- Devuelve un tipo similar a un flujo que itera a través de las filas en los conjuntos de resultados.
use futures::TryStreamExt; // provides `try_next`
use sqlx::Row; // provides `try_get`
let mut rows = sqlx::query("SELECT * FROM users WHERE email = ?")
.bind(email)
.fetch(&mut conn);
while let Some(row) = rows.try_next().await? {
// map the row into a user-defined domain type
let email: &str = row.try_get("email")?;
}