Saltar al contenido principal

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! y query_as!
  • sqlx::query prepara 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 Any permite adaptarse en tiempo de ejecución. AnyPool se conecta al controlador indicado por el esquema de la URL.

Resumen

Query

  • query: Ejecuta consultas genéricas, devuelve filas que se pueden manejar manualmente
  • query_as: Mapea el resultado de la consulta a un struct específico
  • query!: Valida la consulta y los nombres de las columnas en tiempo de compilación, devolviendo filas con acceso por nombre o posición
  • query_as!: Valida la consulta en tiempo de compilación y mapea los resultados a un struct específico

Tipo de solicitudes

  • execute devuelve las filas afectadas (affected_rows) y el resto lo omite
  • fetch devuelve un stream de datos (filas)
  • fetch_one devuelve una fila o error
  • fetch_optional devuelve una fila o None
  • fetch_all devuelve 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)
  • Query y QueryAs se 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 query fuera de execute
sqlx::query("DELETE FROM table").execute(&mut conn).await?;
sqlx::query("DELETE FROM table").execute(&pool).await?;

Consultas no preparadas (simples)

  • &str se 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! y sqlx::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_URL para 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_URL para 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::begin o Connection::begin.
  • Una transacción debe finalizar con una llamada a commit o rollback. Si no se llama a ninguno antes de que la transacción quede fuera de alcance, se llama a rollback.
  • Una transacción se puede utilizar como Executor al 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 implementa sqlx::FromRow para 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")?;
}