Rust Admin

Dominando Banco de Dados em Rust com SQLx e PostgreSQL em Projetos Reais Já leu

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 : Configure uma instância PostgreSQL localmente (via Docker, por exemplo) e crie uma variável de ambiente: Conectando ao Banco de Dados A conexão com PostgreSQL via SQLx é simples e segura. Use para criar um pool de conexões reutilizáveis, essencial em aplicações com alta concorrência: Pools gerenciam automaticamente a reutilização de conexões, melhorando significativamente a performance. A função é assíncrona, por isso a

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.

Referências


Artigos relacionados