Rust

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

8 min de leitura

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

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

<h2>Introdução ao SQLx e PostgreSQL em Rust</h2>

<p>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.</p>

<h3>Configuração inicial do projeto</h3>

<p>Comece criando um novo projeto Rust e adicione as dependências necessárias no <code>Cargo.toml</code>:</p>

<pre><code class="language-toml">[dependencies]

tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }

sqlx = { version = &quot;0.7&quot;, features = [&quot;runtime-tokio-native-tls&quot;, &quot;postgres&quot;] }

serde = { version = &quot;1&quot;, features = [&quot;derive&quot;] }

serde_json = &quot;1&quot;</code></pre>

<p>Configure uma instância PostgreSQL localmente (via Docker, por exemplo) e crie uma variável de ambiente:</p>

<pre><code class="language-bash">export DATABASE_URL=&quot;postgresql://usuario:senha@localhost:5432/meubanco&quot;</code></pre>

<h2>Conectando ao Banco de Dados</h2>

<p>A conexão com PostgreSQL via SQLx é simples e segura. Use <code>PgPoolOptions</code> para criar um pool de conexões reutilizáveis, essencial em aplicações com alta concorrência:</p>

<pre><code class="language-rust">use sqlx::postgres::PgPoolOptions;

#[tokio::main]

async fn main() -&gt; Result&lt;(), sqlx::Error&gt; {

let database_url = std::env::var(&quot;DATABASE_URL&quot;)

.expect(&quot;DATABASE_URL não configurada&quot;);

let pool = PgPoolOptions::new()

.max_connections(5)

.connect(&amp;database_url)

.await?;

println!(&quot;Conectado ao PostgreSQL!&quot;);

Ok(())

}</code></pre>

<p>Pools gerenciam automaticamente a reutilização de conexões, melhorando significativamente a performance. A função <code>connect()</code> é assíncrona, por isso a necessidade do <code>#[tokio::main]</code>. Sempre mantenha o pool vivo enquanto sua aplicação estiver em execução.</p>

<h2>Operações CRUD com SQLx</h2>

<h3>Criar e ler dados</h3>

<p>SQLx oferece dois métodos principais: <code>query!()</code> para verificação em tempo de compilação e <code>query()</code> para queries dinâmicas. O macro <code>query!()</code> requer que a tabela exista no banco durante a compilação:</p>

<pre><code class="language-rust">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: &amp;sqlx::PgPool,

nome: &amp;str,

email: &amp;str,

) -&gt; Result&lt;Usuario, sqlx::Error&gt; {

let usuario = sqlx::query_as::&lt;_, Usuario&gt;(

&quot;INSERT INTO usuarios (nome, email) VALUES ($1, $2)

RETURNING id, nome, email&quot;

)

.bind(nome)

.bind(email)

.fetch_one(pool)

.await?;

Ok(usuario)

}

// Buscar usuário por ID

async fn buscar_usuario(

pool: &amp;sqlx::PgPool,

id: i32,

) -&gt; Result&lt;Option&lt;Usuario&gt;, sqlx::Error&gt; {

let usuario = sqlx::query_as::&lt;_, Usuario&gt;(

&quot;SELECT id, nome, email FROM usuarios WHERE id = $1&quot;

)

.bind(id)

.fetch_optional(pool)

.await?;

Ok(usuario)

}

// Listar todos os usuários

async fn listar_usuarios(

pool: &amp;sqlx::PgPool,

) -&gt; Result&lt;Vec&lt;Usuario&gt;, sqlx::Error&gt; {

let usuarios = sqlx::query_as::&lt;_, Usuario&gt;(

&quot;SELECT id, nome, email FROM usuarios&quot;

)

.fetch_all(pool)

.await?;

Ok(usuarios)

}</code></pre>

<h3>Atualizar e deletar</h3>

<pre><code class="language-rust">// Atualizar usuário

async fn atualizar_usuario(

pool: &amp;sqlx::PgPool,

id: i32,

novo_email: &amp;str,

) -&gt; Result&lt;u64, sqlx::Error&gt; {

let resultado = sqlx::query(

&quot;UPDATE usuarios SET email = $1 WHERE id = $2&quot;

)

.bind(novo_email)

.bind(id)

.execute(pool)

.await?;

Ok(resultado.rows_affected())

}

// Deletar usuário

async fn deletar_usuario(

pool: &amp;sqlx::PgPool,

id: i32,

) -&gt; Result&lt;u64, sqlx::Error&gt; {

let resultado = sqlx::query(

&quot;DELETE FROM usuarios WHERE id = $1&quot;

)

.bind(id)

.execute(pool)

.await?;

Ok(resultado.rows_affected())

}</code></pre>

<h2>Transações e Tratamento de Erros</h2>

<p>Transações garantem consistência de dados em operações complexas. SQLx torna fácil trabalhar com transações:</p>

<pre><code class="language-rust">async fn transferir_saldo(

pool: &amp;sqlx::PgPool,

de_usuario_id: i32,

para_usuario_id: i32,

valor: f64,

) -&gt; Result&lt;(), sqlx::Error&gt; {

let mut tx = pool.begin().await?;

// Débito

sqlx::query(&quot;UPDATE contas SET saldo = saldo - $1 WHERE usuario_id = $2&quot;)

.bind(valor)

.bind(de_usuario_id)

.execute(&amp;mut *tx)

.await?;

// Crédito

sqlx::query(&quot;UPDATE contas SET saldo = saldo + $1 WHERE usuario_id = $2&quot;)

.bind(valor)

.bind(para_usuario_id)

.execute(&amp;mut *tx)

.await?;

tx.commit().await?;

Ok(())

}</code></pre>

<p>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.</p>

<h3>Tratamento robusto de erros</h3>

<pre><code class="language-rust">use sqlx::error::DatabaseError;

match buscar_usuario(pool, 999).await {

Ok(Some(usuario)) =&gt; println!(&quot;Encontrado: {:?}&quot;, usuario),

Ok(None) =&gt; println!(&quot;Usuário não existe&quot;),

Err(e) =&gt; {

eprintln!(&quot;Erro no banco: {}&quot;, e);

// Implementar fallback ou retry

}

}</code></pre>

<h2>Migrations e Versionamento do Schema</h2>

<p>Use <code>sqlx-cli</code> para gerenciar migrations de forma segura:</p>

<pre><code class="language-bash">cargo install sqlx-cli --no-default-features --features postgres

sqlx migrate add -r criar_tabela_usuarios</code></pre>

<p>Arquivo gerado: <code>migrations/20240115120000_criar_tabela_usuarios.up.sql</code></p>

<pre><code class="language-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

);</code></pre>

<p>Execute as migrations:</p>

<pre><code class="language-bash">sqlx migrate run</code></pre>

<p>Migrations mantêm histórico de mudanças e garantem que todos os desenvolvedores trabalhem com o mesmo schema.</p>

<h2>Conclusão</h2>

<p>Dominar SQLx com PostgreSQL em Rust exige compreensão de três pilares: <strong>type-safety em tempo de compilação</strong>, que elimina bugs SQL antes da execução; <strong>async/await para concorrência</strong>, permitindo aplicações de alta performance; e <strong>gerenciamento adequado de transações</strong>, 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/launchbadge/sqlx" target="_blank" rel="noopener noreferrer">Documentação oficial SQLx</a></li>

<li><a href="https://www.postgresql.org/docs/" target="_blank" rel="noopener noreferrer">Documentação PostgreSQL</a></li>

<li><a href="https://tokio.rs/" target="_blank" rel="noopener noreferrer">Tokio Runtime para Rust Assíncrono</a></li>

<li><a href="https://doc.rust-lang.org/book/ch16-00-concurrency.html" target="_blank" rel="noopener noreferrer">Rust Book - Capítulo sobre Concorrência</a></li>

<li><a href="https://github.com/launchbadge/sqlx/tree/main/sqlx-cli" target="_blank" rel="noopener noreferrer">SQLx CLI Tool</a></li>

</ul>

Comentários

Mais em Rust

Drop e o Ciclo de Vida de Recursos em Rust: Do Básico ao Avançado
Drop e o Ciclo de Vida de Recursos em Rust: Do Básico ao Avançado

O Trait Drop e o Ciclo de Vida de Recursos em Rust Rust gerencia memória sem...

Leitura e Escrita de Arquivos em Rust com std::fs: Do Básico ao Avançado
Leitura e Escrita de Arquivos em Rust com std::fs: Do Básico ao Avançado

Introdução ao Módulo std::fs O módulo (filesystem) é a porta de entrada para...

Dominando Slices em Rust: Referências para Partes de Coleções em Projetos Reais
Dominando Slices em Rust: Referências para Partes de Coleções em Projetos Reais

Entendendo Slices em Rust Um slice é uma referência a uma parte contígua de u...