ORM com Diesel: schema e migrations
1. Introdução ao Diesel e configuração do projeto
Diesel é o ORM (Object-Relational Mapping) mais maduro e performático do ecossistema Rust. Ele se destaca por oferecer type safety em tempo de compilação — se sua query está errada, o código nem compila — e zero-cost abstractions, garantindo que o overhead seja mínimo em relação a SQL puro.
Para começar, adicione as dependências no Cargo.toml:
[dependencies]
diesel = { version = "2.1", features = ["postgres"] }
dotenvy = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.axum]
version = "0.7"
features = ["macros"]
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
Instale o Diesel CLI globalmente:
cargo install diesel_cli --no-default-features --features postgres
Crie o arquivo .env na raiz do projeto:
DATABASE_URL=postgres://user:password@localhost/diesel_demo
Inicialize o projeto Diesel:
diesel setup
Esse comando cria o banco de dados (se não existir) e o diretório migrations/.
2. Criação e gerenciamento de migrations
Cada migration é composta por dois arquivos: up.sql (aplicação) e down.sql (rollback). Para criar uma migration de usuários:
diesel migration generate create_users
Isso gera migrations/2024-01-01-120000_create_users/up.sql e seu down.sql. No up.sql:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
No down.sql:
DROP TABLE IF EXISTS users;
Aplique a migration:
diesel migration run
Para reverter: diesel migration redo. Boas práticas: use nomes descritivos como add_role_to_users e mantenha migrations atômicas (uma alteração por migration).
3. Schema gerado automaticamente
Após rodar diesel migration run, o Diesel gera automaticamente src/schema.rs:
// @generated automatically by Diesel CLI.
diesel::table! {
users (id) {
id -> Int4,
name -> Varchar,
email -> Varchar,
created_at -> Nullable<Timestamp>,
}
}
Esse arquivo não deve ser editado manualmente. Para regenerá-lo:
diesel print-schema > src/schema.rs
O mapeamento de tipos é direto: VARCHAR vira String, INTEGER vira i32, TIMESTAMP vira chrono::NaiveDateTime. O Diesel suporta tipos complexos como JSONB via feature flags.
4. Definição de modelos e queries básicas
Crie src/models.rs:
use diesel::prelude::*;
use chrono::NaiveDateTime;
use crate::schema::users;
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
pub created_at: Option<NaiveDateTime>,
}
#[derive(Insertable)]
#[diesel(table_name = users)]
pub struct NewUser {
pub name: String,
pub email: String,
}
Agora, operações CRUD em src/repository.rs:
use diesel::prelude::*;
use crate::models::{User, NewUser};
use crate::schema::users;
pub fn create_user(conn: &mut PgConnection, name: &str, email: &str) -> QueryResult<User> {
let new_user = NewUser {
name: name.to_string(),
email: email.to_string(),
};
diesel::insert_into(users::table)
.values(&new_user)
.returning(User::as_returning())
.get_result(conn)
}
pub fn find_user_by_email(conn: &mut PgConnection, email: &str) -> QueryResult<Option<User>> {
users::table
.filter(users::email.eq(email))
.first(conn)
.optional()
}
pub fn list_users(conn: &mut PgConnection) -> QueryResult<Vec<User>> {
users::table
.order(users::created_at.desc())
.limit(10)
.load(conn)
}
5. Migrations avançadas: alterações e rollbacks
Crie uma migration para adicionar coluna role:
diesel migration generate add_role_to_users
up.sql:
ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'user';
CREATE INDEX idx_users_role ON users(role);
down.sql:
DROP INDEX IF EXISTS idx_users_role;
ALTER TABLE users DROP COLUMN IF EXISTS role;
Para adicionar chave estrangeira:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
body TEXT
);
Cuidado com dados existentes: ao adicionar NOT NULL sem default, a migration falhará se houver registros. Sempre forneça um valor padrão ou use SET NOT NULL após preencher os dados.
6. Trabalhando com conexões e transações
Configure pool de conexões em src/db.rs:
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use dotenvy::dotenv;
use std::env;
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
pub fn establish_pool() -> DbPool {
dotenv().ok();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder()
.max_size(10)
.build(manager)
.expect("Failed to create pool")
}
Transações garantem atomicidade:
use diesel::result::Error;
pub fn create_user_with_post(conn: &mut PgConnection, user_name: &str, post_title: &str) -> Result<(User, Post), Error> {
conn.transaction(|conn| {
let user = create_user(conn, user_name, "user@example.com")?;
let post = create_post(conn, user.id, post_title, "content")?;
Ok((user, post))
})
}
7. Integração com Axum e patterns comuns
Em src/main.rs:
use axum::{extract::State, routing::post, Json, Router};
use std::sync::Arc;
use crate::db::DbPool;
mod db;
mod models;
mod repository;
mod schema;
#[derive(serde::Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
}
async fn create_user_handler(
State(pool): State<Arc<DbPool>>,
Json(body): Json<CreateUserRequest>,
) -> Result<Json<User>, (StatusCode, String)> {
let mut conn = pool.get().map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let user = repository::create_user(&mut conn, &body.name, &body.email)
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
Ok(Json(user))
}
#[tokio::main]
async fn main() {
let pool = Arc::new(db::establish_pool());
let app = Router::new()
.route("/users", post(create_user_handler))
.with_state(pool);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Pattern recomendado para projetos maiores:
src/
├── main.rs
├── db.rs # Pool e conexão
├── models.rs # Structs Queryable/Insertable
├── schema.rs # Gerado automaticamente
├── repository.rs # Queries isoladas
└── handlers.rs # Lógica dos endpoints
Esse layout separa responsabilidades: models define a estrutura, repository contém a lógica de banco, handlers lida com HTTP, e db gerencia o pool.
Referências
- Documentação oficial do Diesel — Guia completo de início rápido com migrations, schema e queries
- Diesel CLI Reference — Comandos detalhados para gerenciamento de migrations
- Diesel Schema Generation — Documentação da macro
table!e geração de schema - Diesel com Axum - Tutorial — Exemplo oficial de integração Diesel + Axum com pool de conexões
- Gerenciamento de Transações no Diesel — API de transações e tratamento de erros
- Diesel Migration Guide — Boas práticas para criação e rollback de migrations