SQLx
Instalaciones
- SQLx
- Para TLS usar
tls-rustls-aws-lc-rsotls-rustls-ring-webpki tls-rustls-aws-lc-rses más pesado y moderno quetls-rustls-ring-webpki
cargo add sqlx --F runtime-tokio,postgres
- Para poder utilizar las variables de entorno del archivo .env
cargo add dotenvy
Variables de entorno
- Crear archivo
.envy agregarlo al.gitignorey.dockerignore - No utilizar comillas dobles en la variable
"url", en local puede funcionar pero en Docker puede fallar
.env
DATABASE_URL=postgres://postgres:123456@localhost:5432/test-db?sslmode=require
Pool
Generalmente se trabaja con un pool de conexiones para no crear y cerrar conexiones constantemente.
db.rs
async fn db_pool() -> Pool<Postgres> {
//dotenv().ok(); // Load .env file (development)
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL undefined");
PgPoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(10)) // Waiting for a connection
.connect(&db_url)
.await
.expect("Failed to connect to PostgreSQL")
}
Agregar el pool al estado
- Se agrega al estado del router con
.with_state() - Así se puede extraer y usar desde cualquier ruta o handler
router.rs
pub async fn router() -> Router {
//let _pool = PgPoolOptions::new()...
// Routes
Router::new()
.route("/test-db", get(test_db))
.route("/users/{id}", get(get_user_by_id))
//.with_state(_pool)
.with_state(db_pool().await)
}
Usar
- Después de agregar el pool al estado, cada ruta tiene acceso al pool
- Se extrae el pool en la firma de la función, ej.
fn T ((State(pool): State<PgPool>)){}
Cuando se utilizan algunos extractores, como Json<T> para el body, este consume la request, entonces, marcará error al querer extraer otros datos, como el Pool si se escribe después de extraer el body.
Ejemplo de error:
Handler<_, ...>` is not satisfied Consider using `#[axum::debug_handler]` to improve the error message
Para evitar errores, usar los extractores que consumen la request después de los extractores que no modifican la request, como State(T)
El orden al usar State, Json, Path, etc. puede provocar errores
Prueba
async fn test_db(State(db_pool): State<PgPool>) -> String {
let res = sqlx::query_scalar::<_, i32>("SELECT 1 + 1 as result")
.fetch_one(&db_pool)
.await;
match res {
Ok(res) => format!("1 + 1: {}", res),
Err(e) => e.to_string(),
}
}
Crear
async fn create_user(
State(pool): State<PgPool>,
Json(payload): Json<CreateUser>, // Extaer body después de State()
) -> (StatusCode, Json<Value>) {
let query_str = "INSERT INTO users(name, age) VALUES ($1, $2) RETURNING id";
let res = sqlx::query(query_str)
.bind(payload.name)
.bind(payload.age)
.fetch_one(&pool)
.await;
match res {
Ok(row) => {
let id: i32 = row.get(0); // use sqlx::Row;
(StatusCode::CREATED, Json(json!({ "user_id": id })))
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(json!({"message": e.to_string()})),
),
}
}
Borrar
async fn delete_user(State(pool): State<PgPool>, Path(id): Path<i32>) -> (StatusCode, Json<Value>) {
if id <= 0 {
return (StatusCode::BAD_REQUEST, Json(json!({"message": "Invalid ID: must be positive"})));
}
let query_result = sqlx::query("DELETE FROM users WHERE id = $1")
.bind(id)
.execute(&pool)
.await;
match query_result {
Ok(result) if result.rows_affected() == 1 => (StatusCode::OK, Json(json!({"message": "Deleted"}))),
Ok(_) => (StatusCode::NOT_FOUND, Json(json!({ "message": "User not found" }))),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"message": e.to_string()})))
}
}
Dos tablas
#[derive(Debug, FromRow)]
struct Author {
pub id: i32,
pub name: String,
}
#[derive(Debug, FromRow)]
struct Book {
pub isbn: String,
pub title: String,
pub price: BigDecimal,
pub author_id: i32,
}
struct BookWithAuthor {
pub isbn: String,
pub title: String,
pub price: BigDecimal,
pub author: Author,
}
async fn get_all_books_with_author(pool: &PgPool) -> Result<Vec<BookWithAuthor>, Box<dyn Error>> {
// Get data and then format
let query =
"SELECT b.isbn, b.title, b.price, a.id, a.name FROM book b JOIN author a ON b.author_id = a.id;";
let rows = sqlx::query(query).fetch_all(pool).await?;
let books_with_author: Vec<BookWithAuthor> = rows
.into_iter()
.map(|row: PgRow| {
let author = Author {
id: row.get("id"),
name: row.get("name"),
};
BookWithAuthor {
isbn: row.get("isbn"),
title: row.get("title"),
price: row.get("price"),
author,
}
})
.collect();
// Get data and format in one step
// let books_with_author = sqlx::query(query)
// .map(|row: PgRow| {
// let author = Author {
// id: row.get("id"),
// name: row.get("name"),
// };
// BookWithAuthor {
// isbn: row.get("isbn"),
// title: row.get("title"),
// price: row.get("price"),
// author,
// }
// })
// .fetch_all(pool)
// .await?;
Ok(books_with_author)
}