
Abaixo veremos duas formas de acessar um banco de dados PostgreSQL em Rust: primeiro sem utilizar um ORM (mapeamento objeto-relacional) e depois utilizando um ORM (neste caso, Diesel). Ambos os exemplos são simples e servem para demonstrar a conexão, inserção e consulta de dados em uma tabela chamada pessoas, com colunas id e nome.
A seguir, temos um exemplo completo de acesso síncrono ao banco de dados PostgreSQL em Rust, sem usar Tokio ou ORM. Usamos a biblioteca postgres (que é bloqueante/síncrona) e uma tabela chamada pessoas:
Cargo.tomlCrie (ou edite) o arquivo Cargo.toml na raiz do projeto, definindo o nome do pacote e adicionando a dependência:
[package]
name = "exemplo_postgres_sincrono_pt"
version = "0.1.0"
edition = "2021"
[dependencies]
postgres = "0.21"
main.rsNo diretório src, crie (ou edite) um arquivo chamado main.rs:
use postgres::{Client, NoTls, Error};
fn main() -> Result<(), Error> {
// 1. Conectar ao banco de dados de forma síncrona
// Ajuste a string de conexão conforme seu ambiente (host, user, password, dbname).
let mut cliente = Client::connect(
"host=localhost user=postgres password=postgres dbname=exemplo_rust",
NoTls,
)?;
// 2. Criar a tabela "pessoas" se ela não existir
// Aqui, criamos duas colunas: id (chave primária) e nome (VARCHAR).
cliente.execute(
"CREATE TABLE IF NOT EXISTS pessoas (
id SERIAL PRIMARY KEY,
nome VARCHAR NOT NULL
)",
&[],
)?;
// 3. Inserir dados na tabela
// Usamos placeholders ($1) para evitar SQL injection.
cliente.execute(
"INSERT INTO pessoas (nome) VALUES ($1)",
&[&"João da Silva"],
)?;
// 4. Consultar dados da tabela
// A função query retorna um vetor de linhas (Row).
let linhas_encontradas = cliente.query("SELECT id, nome FROM pessoas", &[])?;
// 5. Exibir os resultados obtidos
for linha in linhas_encontradas {
// "linha.get(0)" retorna o valor da primeira coluna (id), do tipo i32
let identificador: i32 = linha.get(0);
// "linha.get(1)" retorna o valor da segunda coluna (nome), do tipo String
let nome_da_pessoa: String = linha.get(1);
println!("ID: {}, Nome: {}", identificador, nome_da_pessoa);
}
// 6. Retornar Ok(()) indicando sucesso
Ok(())
}
Client, NoTls e Error da crate postgres.
Client é a estrutura principal para gerenciar a conexão síncrona.NoTls indica que não estamos usando nenhuma camada de criptografia (TLS) adicional.Error é o tipo de erro que pode ser retornado pelas funções de banco.fn main() -> Result<(), Error>: Definimos a função principal para retornar um Result; caso algo dê errado, retornamos um Error da crate postgres.
Client::connect(...): Cria uma conexão bloqueante (síncrona) com o Postgres usando as credenciais passadas na string. Se não for possível conectar, a função retorna um erro (? propaga o erro para main).
Criação da tabela: Executamos um comando SQL simples, CREATE TABLE IF NOT EXISTS pessoas (...), usando cliente.execute(...). O retorno (número de linhas afetadas) não é tão relevante, por isso descartamos.
Inserção de dados: Fazemos INSERT INTO pessoas (nome) VALUES ($1), passando &"João da Silva" como parâmetro. Usar placeholders ($1, $2, etc.) ajuda a evitar SQL injection e facilita o binding de parâmetros.
Consulta: Chamamos cliente.query(...) para executar SELECT id, nome FROM pessoas. Recebemos um vetor de linhas (Row).
Leitura de colunas: Para cada linha, fazemos linha.get(0) ou linha.get("nome") para obter os dados. Mapeamos para i32 (no caso do id) e String (para nome).
Exibição: Imprimimos ID: ..., Nome: ... no console.
main termina, o objeto cliente é descartado, e a conexão é finalizada.exemplo_rust (ou ajuste a string de conexão no código).cargo run
Isso demonstra o acesso síncrono ao PostgreSQL, sem usar Tokio (ou async/await) e sem um ORM (só consultas SQL puras).
Agora vamos usar o Diesel, um ORM para Rust. Ele gera e gerencia consultas em Rust de forma mais expressiva e segura em tempo de compilação.
sudo apt-get update
sudo apt-get install -y build-essential libpq-dev pkg-config libssl-dev
cargo install diesel_cli --no-default-features --features postgres
.env com a URL de conexão ao seu banco de dados. Exemplo:
DATABASE_URL=postgres://postgres:postgres@localhost/exemplo_rust
Cargo.toml[package]
name = "exemplo_postgres_orm"
version = "0.1.0"
edition = "2021"
[dependencies]
diesel = { version = "2.2.6", features = ["postgres"] }
dotenvy = "0.15" # Usado para ler variáveis do .env
A estrutura típica de um projeto com Diesel (utilizando diesel_cli) inclui:
exemplo_postgres_orm
├── Cargo.toml
├── .env
├── migrations
│ └── YYYY-MM-DD-nnnn_nome_migration
│ ├── up.sql
│ └── down.sql
└── src
├── main.rs
└── schema.rs
Usando o Diesel CLI, podemos gerar uma migration “em branco”:
diesel migration generate criar_pessoas
Isso criará uma pasta em migrations com timestamp, por exemplo:
migrations/
└── 2023-01-01-000000_criar_pessoas
├── up.sql
└── down.sql
up.sql e down.sqlNo up.sql (migração “pra cima”), definimos a criação da tabela:
DROP TABLE IF EXISTS pessoas;
CREATE TABLE pessoas (
id SERIAL PRIMARY KEY,
nome VARCHAR NOT NULL
);
No down.sql (migração “pra baixo”), definimos como desfazer essa criação:
DROP TABLE pessoas;
Agora executamos:
diesel migration run
O Diesel vai:
DATABASE_URL do .env.pessoas se não existir (em exemplo_rust, no caso).__diesel_schema_migrations, por padrão).Verifique se a tabela foi criada, por exemplo, usando o psql ou outro cliente:
psql -h localhost -U postgres -d postgres -c "\dt"
Deverá constar pessoas lá.
schema.rsO Diesel mapeia as tabelas para código Rust via o “schema”. Podemos gerar automaticamente o conteúdo do schema.rs com:
diesel print-schema > src/schema.rs
Evite criar o
schema.rsmanualmente, pois perderá muito tempo.
Isso analisará o banco (via DATABASE_URL) e gerará algo assim:
// src/schema.rs
diesel::table! {
pessoas (id) {
id -> Int4,
nome -> Varchar,
}
}
Este arquivo não deve ser editado manualmente se você usa
print-schemaregularmente. Caso contrário, pode manter manualmente sincronizado.
O arquivo
schema.rsdescreve o mapeamento das tabelas do banco (estruturas e tipos de cada coluna) e normalmente é gerado automaticamente pelo Diesel. O arquivomodels.rsdefine as structs Rust que representam as linhas dessas tabelas, por exemploPessoaeNovaPessoa, com#[derive(Queryable)]ou#[derive(Insertable)], vinculando essas structs ao que foi definido emschema.rs.
**Por que duas structs para a mesma tabela?
Precisamos de duas structs porque, ao inserir dados, nem sempre queremos (ou podemos) incluir valores que são gerados automaticamente (como o
id), enquanto, ao carregar dados (fazer SELECT), precisamos de todos os campos que a tabela retorna. Assim,NovaPessoanormalmente omite campos auto-gerados, enquantoPessoarepresenta a linha completa no banco, incluindo oid.
Na pasta src, crie (ou edite) um arquivo models.rs para colocar as estruturas que representam a tabela. Exemplo:
use super::schema::pessoas; // "super" pois o schema.rs está em src, mesmo nível
// Representação de uma linha na tabela (para SELECT).
// A trait Queryable permite ao Diesel "popular" esse struct ao fazer a query.
#[derive(Queryable)]
pub struct Pessoa {
pub id: i32,
pub nome: String,
}
// Representação dos dados que inserimos (INSERT).
#[derive(Insertable)]
#[diesel(table_name = pessoas)]
pub struct NovaPessoa<'a> {
pub nome: &'a str,
}
Note que
NovaPessoanão temid, pois oidé gerado automaticamente (SERIAL).
main.rsPor fim, criamos o main.rs para:
pessoas.mod schema;
mod models;
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;
// Importa as coisas que vamos usar
use crate::models::{NovaPessoa, Pessoa};
use crate::schema::pessoas::dsl::*;
fn estabelecer_conexao() -> PgConnection {
dotenv().ok(); // Lê variáveis do arquivo .env
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL não definida no .env");
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Falha ao conectar em {}", database_url))
}
fn main() {
// 1. Cria a conexão mutável
let mut conexao = estabelecer_conexao();
// 2. Insere uma nova pessoa
let maria = NovaPessoa { nome: "Maria das Couves" };
diesel::insert_into(pessoas)
.values(&maria)
// repare que passamos &mut conexao
.execute(&mut conexao)
.expect("Erro ao inserir pessoa");
// 3. Consulta a tabela
let resultado = pessoas
.load::<Pessoa>(&mut conexao) // &mut conexao
.expect("Erro ao carregar pessoas");
println!("Lista de pessoas:");
for p in resultado {
println!("ID: {}, Nome: {}", p.id, p.nome);
}
}
No terminal, dentro da pasta do projeto:
cargo run
.env para conectar ao PostgreSQL.main.rs, que insere “Maria das Couves” na tabela pessoas.id e nome de cada registro no terminal.Cargo.toml: inclua as dependências diesel e dotenvy..env com DATABASE_URL.diesel migration generate <nome>), escreva o SQL em up.sql/down.sql, e rode diesel migration run.diesel print-schema > src/schema.rs).models.rs, definindo structs com #[derive(Queryable)], #[derive(Insertable)], etc.).main.rs (ou em outro binário) para usar Diesel: