Introdução ao SQLx e PostgreSQL em Rust
SQLx é um driver SQL assincrônico e type-safe para Rust que funciona com PostgreSQL, MySQL e SQLite. Diferentemente de ORMs tradicionais, SQLx realiza verificação de tipos em tempo de compilação contra o banco de dados real, eliminando erros SQL durante o desenvolvimento. PostgreSQL é o banco relacional mais robusto do mercado, oferecendo recursos avançados como JSON, arrays e extensões custom. Juntos, formam a combinação ideal para aplicações Rust modernas que exigem confiabilidade e performance.
Configuração inicial do projeto
Comece criando um novo projeto Rust e adicione as dependências necessárias no Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Configure uma instância PostgreSQL localmente (via Docker, por exemplo) e crie uma variável de ambiente:
export DATABASE_URL="postgresql://usuario:senha@localhost:5432/meubanco"
Conectando ao Banco de Dados
A conexão com PostgreSQL via SQLx é simples e segura. Use PgPoolOptions para criar um pool de conexões reutilizáveis, essencial em aplicações com alta concorrência:
use sqlx::postgres::PgPoolOptions;
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL não configurada");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
println!("Conectado ao PostgreSQL!");
Ok(())
}
Pools gerenciam automaticamente a reutilização de conexões, melhorando significativamente a performance. A função connect() é assíncrona, por isso a necessidade do #[tokio::main]. Sempre mantenha o pool vivo enquanto sua aplicação estiver em execução.
Operações CRUD com SQLx
Criar e ler dados
SQLx oferece dois métodos principais: query!() para verificação em tempo de compilação e query() para queries dinâmicas. O macro query!() requer que a tabela exista no banco durante a compilação:
use sqlx::{Row, FromRow};
use serde::{Deserialize, Serialize};
#[derive(Debug, FromRow, Serialize, Deserialize)]
struct Usuario {
id: i32,
nome: String,
email: String,
}
// Criar um usuário
async fn criar_usuario(
pool: &sqlx::PgPool,
nome: &str,
email: &str,
) -> Result<Usuario, sqlx::Error> {
let usuario = sqlx::query_as::<_, Usuario>(
"INSERT INTO usuarios (nome, email) VALUES ($1, $2)
RETURNING id, nome, email"
)
.bind(nome)
.bind(email)
.fetch_one(pool)
.await?;
Ok(usuario)
}
// Buscar usuário por ID
async fn buscar_usuario(
pool: &sqlx::PgPool,
id: i32,
) -> Result<Option<Usuario>, sqlx::Error> {
let usuario = sqlx::query_as::<_, Usuario>(
"SELECT id, nome, email FROM usuarios WHERE id = $1"
)
.bind(id)
.fetch_optional(pool)
.await?;
Ok(usuario)
}
// Listar todos os usuários
async fn listar_usuarios(
pool: &sqlx::PgPool,
) -> Result<Vec<Usuario>, sqlx::Error> {
let usuarios = sqlx::query_as::<_, Usuario>(
"SELECT id, nome, email FROM usuarios"
)
.fetch_all(pool)
.await?;
Ok(usuarios)
}
Atualizar e deletar
// Atualizar usuário
async fn atualizar_usuario(
pool: &sqlx::PgPool,
id: i32,
novo_email: &str,
) -> Result<u64, sqlx::Error> {
let resultado = sqlx::query(
"UPDATE usuarios SET email = $1 WHERE id = $2"
)
.bind(novo_email)
.bind(id)
.execute(pool)
.await?;
Ok(resultado.rows_affected())
}
// Deletar usuário
async fn deletar_usuario(
pool: &sqlx::PgPool,
id: i32,
) -> Result<u64, sqlx::Error> {
let resultado = sqlx::query(
"DELETE FROM usuarios WHERE id = $1"
)
.bind(id)
.execute(pool)
.await?;
Ok(resultado.rows_affected())
}
Transações e Tratamento de Erros
Transações garantem consistência de dados em operações complexas. SQLx torna fácil trabalhar com transações:
async fn transferir_saldo(
pool: &sqlx::PgPool,
de_usuario_id: i32,
para_usuario_id: i32,
valor: f64,
) -> Result<(), sqlx::Error> {
let mut tx = pool.begin().await?;
// Débito
sqlx::query("UPDATE contas SET saldo = saldo - $1 WHERE usuario_id = $2")
.bind(valor)
.bind(de_usuario_id)
.execute(&mut *tx)
.await?;
// Crédito
sqlx::query("UPDATE contas SET saldo = saldo + $1 WHERE usuario_id = $2")
.bind(valor)
.bind(para_usuario_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
Se qualquer operação falhar, a transação é automaticamente revertida. Sempre use transações para operações que envolvem múltiplas tabelas ou garantias de integridade.
Tratamento robusto de erros
use sqlx::error::DatabaseError;
match buscar_usuario(pool, 999).await {
Ok(Some(usuario)) => println!("Encontrado: {:?}", usuario),
Ok(None) => println!("Usuário não existe"),
Err(e) => {
eprintln!("Erro no banco: {}", e);
// Implementar fallback ou retry
}
}
Migrations e Versionamento do Schema
Use sqlx-cli para gerenciar migrations de forma segura:
cargo install sqlx-cli --no-default-features --features postgres
sqlx migrate add -r criar_tabela_usuarios
Arquivo gerado: migrations/20240115120000_criar_tabela_usuarios.up.sql
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Execute as migrations:
sqlx migrate run
Migrations mantêm histórico de mudanças e garantem que todos os desenvolvedores trabalhem com o mesmo schema.
Conclusão
Dominar SQLx com PostgreSQL em Rust exige compreensão de três pilares: type-safety em tempo de compilação, que elimina bugs SQL antes da execução; async/await para concorrência, permitindo aplicações de alta performance; e gerenciamento adequado de transações, garantindo integridade dos dados. Pratique com projetos reais, use migrations desde o início e sempre valide dados na camada de aplicação. Com esses fundamentos sólidos, você construirá sistemas de banco de dados confiáveis e mantíveis em Rust.