Introdução
Ao trabalhar com bancos de dados em Rust, duas bibliotecas se destacam: Diesel e SQLx. Embora ambas resolvam o mesmo problema — comunicar sua aplicação com o banco de dados — elas adotam abordagens fundamentalmente diferentes.
Diesel é um ORM (Object-Relational Mapper) completo com um DSL de queries fortemente tipado. Ele gera SQL a partir de código Rust e verifica tudo em tempo de compilação usando seu schema DSL. É síncrono por padrão, embora suporte async via diesel-async.
SQLx é um query builder que executa SQL puro, mas com uma característica única: ele verifica suas queries SQL contra um banco de dados real em tempo de compilação usando a macro query!. É async-first e suporta múltiplos runtimes.
Ambas garantem type safety em tempo de compilação, mas de maneiras completamente distintas. Vamos explorar essas diferenças em detalhes.
Tabela Comparativa
| Característica | Diesel | SQLx |
|---|---|---|
| Tipo | ORM completo | Query builder / SQL puro |
| Async | Via diesel-async | Nativo |
| Verificação compile-time | Via schema DSL | Via macro query! (requer DB) |
| Migrations | CLI diesel migration | CLI sqlx migrate |
| Bancos suportados | PostgreSQL, MySQL, SQLite | PostgreSQL, MySQL, SQLite |
| DSL de queries | Sim (Rust puro) | Não (SQL puro) |
| Pool de conexões | Via r2d2 ou deadpool | Integrado (PgPool) |
| Curva de aprendizado | Alta (DSL próprio) | Moderada (SQL direto) |
| Downloads mensais | ~7M+ | ~12M+ |
Dependências no Cargo.toml
Para Diesel (com PostgreSQL):
[dependencies]
diesel = { version = "2", features = ["postgres"] }
dotenvy = "0.15"
# Para async (opcional)
diesel-async = { version = "0.5", features = ["postgres", "deadpool"] }
tokio = { version = "1", features = ["full"] }
Para SQLx (com PostgreSQL):
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid", "migrate"] }
tokio = { version = "1", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
Setup e Migrations
Diesel
O Diesel usa uma CLI dedicada para gerenciar o schema:
# Instalar a CLI
cargo install diesel_cli --no-default-features --features postgres
# Configurar
echo "DATABASE_URL=postgres://user:pass@localhost/meu_banco" > .env
diesel setup
# Criar migration
diesel migration generate criar_tarefas
O arquivo de migration (SQL):
-- up.sql
CREATE TABLE tarefas (
id SERIAL PRIMARY KEY,
titulo VARCHAR NOT NULL,
descricao TEXT,
concluida BOOLEAN NOT NULL DEFAULT FALSE,
criada_em TIMESTAMP NOT NULL DEFAULT NOW()
);
-- down.sql
DROP TABLE tarefas;
Após rodar diesel migration run, o Diesel gera automaticamente um arquivo src/schema.rs:
// src/schema.rs (gerado automaticamente)
diesel::table! {
tarefas (id) {
id -> Int4,
titulo -> Varchar,
descricao -> Nullable<Text>,
concluida -> Bool,
criada_em -> Timestamp,
}
}
SQLx
O SQLx também tem uma CLI:
# Instalar a CLI
cargo install sqlx-cli --no-default-features --features postgres
# Criar banco
sqlx database create
# Criar migration
sqlx migrate add criar_tarefas
A migration é SQL puro:
-- migrations/20260223000000_criar_tarefas.sql
CREATE TABLE tarefas (
id SERIAL PRIMARY KEY,
titulo VARCHAR NOT NULL,
descricao TEXT,
concluida BOOLEAN NOT NULL DEFAULT FALSE,
criada_em TIMESTAMP NOT NULL DEFAULT NOW()
);
// Executar migrations no código
sqlx::migrate!("./migrations")
.run(&pool)
.await?;
Comparação de Código: CRUD Completo
Models
Diesel:
use diesel::prelude::*;
use crate::schema::tarefas;
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = tarefas)]
pub struct Tarefa {
pub id: i32,
pub titulo: String,
pub descricao: Option<String>,
pub concluida: bool,
pub criada_em: chrono::NaiveDateTime,
}
#[derive(Insertable)]
#[diesel(table_name = tarefas)]
pub struct NovaTarefa<'a> {
pub titulo: &'a str,
pub descricao: Option<&'a str>,
}
SQLx:
use sqlx::FromRow;
use chrono::NaiveDateTime;
#[derive(Debug, FromRow)]
pub struct Tarefa {
pub id: i32,
pub titulo: String,
pub descricao: Option<String>,
pub concluida: bool,
pub criada_em: NaiveDateTime,
}
No SQLx, não há necessidade de um struct separado para inserção — os valores são passados diretamente como parâmetros da query.
INSERT
Diesel:
use diesel::prelude::*;
use crate::schema::tarefas;
use crate::models::{Tarefa, NovaTarefa};
fn criar_tarefa(conn: &mut PgConnection, titulo: &str, desc: Option<&str>) -> Tarefa {
let nova = NovaTarefa {
titulo,
descricao: desc,
};
diesel::insert_into(tarefas::table)
.values(&nova)
.returning(Tarefa::as_returning())
.get_result(conn)
.expect("Erro ao inserir tarefa")
}
SQLx:
use sqlx::PgPool;
use crate::models::Tarefa;
async fn criar_tarefa(pool: &PgPool, titulo: &str, desc: Option<&str>) -> Result<Tarefa, sqlx::Error> {
sqlx::query_as::<_, Tarefa>(
"INSERT INTO tarefas (titulo, descricao) VALUES ($1, $2) RETURNING *"
)
.bind(titulo)
.bind(desc)
.fetch_one(pool)
.await
}
Ou com a macro verificada em tempo de compilação:
async fn criar_tarefa(pool: &PgPool, titulo: &str, desc: Option<&str>) -> Result<Tarefa, sqlx::Error> {
sqlx::query_as!(
Tarefa,
"INSERT INTO tarefas (titulo, descricao) VALUES ($1, $2) RETURNING *",
titulo,
desc
)
.fetch_one(pool)
.await
}
SELECT
Diesel:
use diesel::prelude::*;
use crate::schema::tarefas::dsl::*;
use crate::models::Tarefa;
fn listar_tarefas(conn: &mut PgConnection) -> Vec<Tarefa> {
tarefas
.filter(concluida.eq(false))
.order(criada_em.desc())
.limit(10)
.select(Tarefa::as_select())
.load(conn)
.expect("Erro ao buscar tarefas")
}
fn buscar_por_id(conn: &mut PgConnection, tarefa_id: i32) -> Option<Tarefa> {
tarefas
.find(tarefa_id)
.select(Tarefa::as_select())
.first(conn)
.optional()
.expect("Erro ao buscar tarefa")
}
SQLx:
use sqlx::PgPool;
use crate::models::Tarefa;
async fn listar_tarefas(pool: &PgPool) -> Result<Vec<Tarefa>, sqlx::Error> {
sqlx::query_as!(
Tarefa,
"SELECT * FROM tarefas WHERE concluida = false ORDER BY criada_em DESC LIMIT 10"
)
.fetch_all(pool)
.await
}
async fn buscar_por_id(pool: &PgPool, tarefa_id: i32) -> Result<Option<Tarefa>, sqlx::Error> {
sqlx::query_as!(
Tarefa,
"SELECT * FROM tarefas WHERE id = $1",
tarefa_id
)
.fetch_optional(pool)
.await
}
UPDATE
Diesel:
use diesel::prelude::*;
use crate::schema::tarefas::dsl::*;
use crate::models::Tarefa;
fn concluir_tarefa(conn: &mut PgConnection, tarefa_id: i32) -> Option<Tarefa> {
diesel::update(tarefas.find(tarefa_id))
.set(concluida.eq(true))
.returning(Tarefa::as_returning())
.get_result(conn)
.optional()
.expect("Erro ao atualizar tarefa")
}
SQLx:
async fn concluir_tarefa(pool: &PgPool, tarefa_id: i32) -> Result<Option<Tarefa>, sqlx::Error> {
sqlx::query_as!(
Tarefa,
"UPDATE tarefas SET concluida = true WHERE id = $1 RETURNING *",
tarefa_id
)
.fetch_optional(pool)
.await
}
DELETE
Diesel:
use diesel::prelude::*;
use crate::schema::tarefas::dsl::*;
fn deletar_tarefa(conn: &mut PgConnection, tarefa_id: i32) -> usize {
diesel::delete(tarefas.find(tarefa_id))
.execute(conn)
.expect("Erro ao deletar tarefa")
}
SQLx:
async fn deletar_tarefa(pool: &PgPool, tarefa_id: i32) -> Result<u64, sqlx::Error> {
let result = sqlx::query!("DELETE FROM tarefas WHERE id = $1", tarefa_id)
.execute(pool)
.await?;
Ok(result.rows_affected())
}
Conexão e Pool
Diesel (síncrono com r2d2):
use diesel::pg::PgConnection;
use diesel::r2d2::{self, ConnectionManager};
type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
fn criar_pool(database_url: &str) -> DbPool {
let manager = ConnectionManager::<PgConnection>::new(database_url);
r2d2::Pool::builder()
.max_size(15)
.build(manager)
.expect("Falha ao criar pool")
}
SQLx (async nativo):
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
async fn criar_pool(database_url: &str) -> PgPool {
PgPoolOptions::new()
.max_connections(15)
.connect(database_url)
.await
.expect("Falha ao criar pool")
}
Performance e Compilação
Tempo de compilação
O Diesel gera toda a verificação através do schema DSL em Rust — não precisa de conexão com o banco durante a compilação. Já o SQLx com a macro query! precisa de acesso ao banco em tempo de compilação (ou de um cache gerado com cargo sqlx prepare).
Para CI/CD, o SQLx oferece o modo offline:
# Gerar cache de metadados
cargo sqlx prepare
# No CI, compilar sem banco de dados
SQLX_OFFLINE=true cargo build
Performance em runtime
Ambas as bibliotecas geram queries SQL eficientes. O Diesel pode ter uma leve vantagem em cenários com queries simples, pois o SQL é gerado estaticamente. O SQLx envia SQL literal, o que pode ser mais eficiente para queries complexas que você otimizou manualmente.
Na prática, o gargalo de performance é quase sempre o banco de dados, não a biblioteca Rust.
Quando Escolher Cada Um
Escolha Diesel quando:
- Prefere um DSL Rust-nativo ao invés de escrever SQL
- Quer verificação em tempo de compilação sem banco de dados conectado
- Precisa de suporte maduro com anos de uso em produção
- Trabalha com uma equipe que não domina SQL avançado
- Prefere patterns de ORM tradicionais (ActiveRecord-like)
Escolha SQLx quando:
- Prefere escrever SQL puro e ter controle total sobre as queries
- Precisa de suporte async nativo integrado com Tokio
- Quer verificação de SQL contra o banco real em tempo de compilação
- Trabalha com queries complexas (JOINs, CTEs, window functions)
- Está usando um framework async como Axum
- Precisa de features específicas do banco (jsonb, arrays, etc.)
Recomendação prática
Para projetos novos em 2026 com Axum ou outro framework async, SQLx tende a ser a melhor escolha pela integração nativa com async e pela flexibilidade do SQL puro. Para projetos que valorizam abstração sobre o banco e patterns de ORM, o Diesel continua sendo excelente.
Também vale considerar o SeaORM, que combina o melhor dos dois mundos: uma DSL de queries com suporte async nativo sobre SQLx.
Guia de Migração: Diesel para SQLx
Se você está migrando do Diesel para o SQLx:
- Migrations: As migrations são SQL puro em ambos — copie os arquivos
.sqldiretamente - Models: Troque
#[derive(Queryable)]por#[derive(FromRow)] - Queries: Converta as queries do DSL Diesel para SQL puro nas macros
query!/query_as! - Pool: Substitua
r2d2::PoolporPgPooldo SQLx - Schema: Remova o
schema.rs— não é mais necessário - Prepare offline: Execute
cargo sqlx preparepara CI
Conclusão
Diesel e SQLx representam duas filosofias válidas para acesso a bancos de dados em Rust. O Diesel oferece a segurança de um DSL fortemente tipado, enquanto o SQLx combina SQL puro com verificação em tempo de compilação. Ambos são excelentes ferramentas — a escolha depende do estilo da sua equipe e das necessidades do projeto.