Database com SQLx: queries type-safe em compile-time
1. Introdução ao SQLx e suas vantagens sobre outras ORMs
SQLx é uma crate assíncrona e nativa em Rust que permite escrever queries SQL manualmente com verificação de tipos em tempo de compilação. Diferente de ORMs como Diesel, que exigem um schema rígido definido em arquivos de migração e uma estrutura de tipos complexa, o SQLx oferece flexibilidade total do SQL puro combinado com segurança de tipos.
Enquanto Diesel força você a trabalhar com uma DSL própria e Redis serve apenas para cache simples, o SQLx preenche o espaço ideal: você escreve SQL real, mas o compilador verifica se as colunas existem, se os tipos são compatíveis e se os parâmetros estão corretos — tudo antes do runtime.
Os benefícios principais incluem zero overhead em tempo de execução (as macros são resolvidas durante a compilação), suporte nativo a PostgreSQL, MySQL e SQLite, e integração perfeita com runtimes assíncronos como Tokio e async-std.
2. Configuração do ambiente e conexão com o banco
Para começar, adicione as dependências no Cargo.toml:
[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
anyhow = "1"
Crie um arquivo .env na raiz do projeto:
DATABASE_URL=postgres://user:password@localhost:5432/meu_banco
Agora, configure o pool de conexões:
use sqlx::postgres::PgPool;
use dotenvy::dotenv;
use std::env;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
dotenv().ok();
let database_url = env::var("DATABASE_URL")?;
let pool = PgPool::connect(&database_url).await?;
println!("Conectado ao banco!");
Ok(())
}
3. Migrações com SQLx CLI
Instale o sqlx-cli globalmente:
cargo install sqlx-cli
Crie a estrutura de migrações:
sqlx migrate add criar_tabela_usuarios
Isso gera um arquivo na pasta migrations/ com formato 20240101000000_criar_tabela_usuarios.sql. Edite o arquivo:
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
criado_em TIMESTAMPTZ DEFAULT NOW()
);
Execute as migrações:
sqlx migrate run
Para reverter a última migração:
sqlx migrate revert
4. Queries type-safe em compile-time com query! e query_as!
O poder do SQLx está nas macros que verificam as queries durante a compilação. Para isso, o SQLx precisa se conectar ao banco durante o build. Configure no .env e o compilador fará o resto.
Exemplo com query!:
use sqlx::query;
async fn buscar_usuario(pool: &PgPool, user_id: i32) -> Result<(), sqlx::Error> {
let row = query!("SELECT id, nome, email FROM usuarios WHERE id = $1", user_id)
.fetch_one(pool)
.await?;
println!("ID: {}, Nome: {}, Email: {}", row.id, row.nome, row.email);
Ok(())
}
Se você tentar acessar uma coluna que não existe ou usar um tipo incompatível, o compilador apontará o erro imediatamente:
// Isso não compila se a coluna "idade" não existir na tabela
let row = query!("SELECT idade FROM usuarios WHERE id = $1", user_id)
.fetch_one(pool)
.await?;
5. Trabalhando com estruturas customizadas e FromRow
Para mapear resultados em structs tipadas, use FromRow:
use sqlx::FromRow;
#[derive(Debug, FromRow)]
struct Usuario {
id: i32,
nome: String,
email: String,
}
async fn listar_usuarios(pool: &PgPool) -> Result<Vec<Usuario>, sqlx::Error> {
let usuarios = sqlx::query_as::<_, Usuario>(
"SELECT id, nome, email FROM usuarios ORDER BY id"
)
.fetch_all(pool)
.await?;
Ok(usuarios)
}
A macro query_as! oferece ainda mais segurança:
async fn buscar_por_email(pool: &PgPool, email: &str) -> Result<Option<Usuario>, sqlx::Error> {
let usuario = sqlx::query_as!(
Usuario,
"SELECT id, nome, email FROM usuarios WHERE email = $1",
email
)
.fetch_optional(pool)
.await?;
Ok(usuario)
}
6. Operações CRUD completas com SQLx
Create
async fn criar_usuario(pool: &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",
nome,
email
)
.fetch_one(pool)
.await?;
Ok(usuario)
}
Read com filtros dinâmicos
async fn buscar_por_nome(pool: &PgPool, termo: &str) -> Result<Vec<Usuario>, sqlx::Error> {
let usuarios = sqlx::query_as!(
Usuario,
"SELECT id, nome, email FROM usuarios WHERE nome ILIKE $1",
format!("%{}%", termo)
)
.fetch_all(pool)
.await?;
Ok(usuarios)
}
Update e Delete
async fn atualizar_email(pool: &PgPool, id: i32, novo_email: &str) -> Result<u64, sqlx::Error> {
let linhas_afetadas = sqlx::query!(
"UPDATE usuarios SET email = $1 WHERE id = $2",
novo_email,
id
)
.execute(pool)
.await?
.rows_affected();
Ok(linhas_afetadas)
}
async fn deletar_usuario(pool: &PgPool, id: i32) -> Result<bool, sqlx::Error> {
let resultado = sqlx::query!("DELETE FROM usuarios WHERE id = $1", id)
.execute(pool)
.await?;
Ok(resultado.rows_affected() > 0)
}
7. Transações e concorrência segura
Transações garantem consistência ACID em operações que envolvem múltiplas queries:
async fn transferencia(
pool: &PgPool,
origem_id: i32,
destino_id: i32,
valor: f64,
) -> Result<(), sqlx::Error> {
let mut tx = pool.begin().await?;
sqlx::query!(
"UPDATE contas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1",
valor,
origem_id
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"UPDATE contas SET saldo = saldo + $1 WHERE id = $2",
valor,
destino_id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
Se qualquer operação falhar, o Transaction é automaticamente descartado (rollback) ao sair do escopo, ou você pode chamar tx.rollback().await? explicitamente.
8. Boas práticas e considerações finais
Paginação segura: sempre use parâmetros vinculados para OFFSET e LIMIT para evitar SQL injection:
async fn listar_paginado(pool: &PgPool, pagina: i64, por_pagina: i64) -> Result<Vec<Usuario>, sqlx::Error> {
let offset = (pagina - 1) * por_pagina;
let usuarios = sqlx::query_as!(
Usuario,
"SELECT id, nome, email FROM usuarios ORDER BY id LIMIT $1 OFFSET $2",
por_pagina,
offset
)
.fetch_all(pool)
.await?;
Ok(usuarios)
}
Cache com Redis: para queries frequentes, combine SQLx com redis crate. Use SQLx para escrita e Redis para leitura de dados que mudam pouco.
Testes com banco em memória: SQLite em modo :memory: é excelente para testes rápidos:
#[cfg(test)]
mod tests {
use sqlx::SqlitePool;
#[sqlx::test]
async fn test_criar_usuario(pool: SqlitePool) -> sqlx::Result<()> {
sqlx::query("CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT, email TEXT)")
.execute(&pool)
.await?;
sqlx::query("INSERT INTO usuarios (nome, email) VALUES ('Teste', 'teste@email.com')")
.execute(&pool)
.await?;
Ok(())
}
}
SQLx oferece o melhor dos dois mundos: a flexibilidade do SQL puro com a segurança de tipos que Rust proporciona. Para projetos que exigem controle fino sobre queries e performance previsível, é a escolha ideal.
Referências
- Documentação oficial do SQLx — Referência completa da API, incluindo macros, traits e tipos suportados
- SQLx GitHub Repository — Código fonte, issues, exemplos e guias de contribuição
- SQLx CLI Guide — Guia oficial de instalação e uso do sqlx-cli para migrações
- The Rust SQL Toolkit - SQLx Book — Exemplos práticos de integração com PostgreSQL, MySQL e SQLite
- Building a REST API with Axum and SQLx — Tutorial completo de construção de API REST com Axum, SQLx e Tokio