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.toml
Crie (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.rs
No 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.sql
No 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.rs
O 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.rs
manualmente, 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-schema
regularmente. Caso contrário, pode manter manualmente sincronizado.
O arquivo
schema.rs
descreve o mapeamento das tabelas do banco (estruturas e tipos de cada coluna) e normalmente é gerado automaticamente pelo Diesel. O arquivomodels.rs
define as structs Rust que representam as linhas dessas tabelas, por exemploPessoa
eNovaPessoa
, 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,NovaPessoa
normalmente omite campos auto-gerados, enquantoPessoa
representa 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
NovaPessoa
não temid
, pois oid
é gerado automaticamente (SERIAL).
main.rs
Por 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: