Diesel vs SQLx: Qual ORM Rust Usar em 2026? | Rust Brasil

Diesel vs SQLx: ORM vs query builder em Rust. Verificação em compile-time, async, migrations e type safety. Qual escolher?

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ísticaDieselSQLx
TipoORM completoQuery builder / SQL puro
AsyncVia diesel-asyncNativo
Verificação compile-timeVia schema DSLVia macro query! (requer DB)
MigrationsCLI diesel migrationCLI sqlx migrate
Bancos suportadosPostgreSQL, MySQL, SQLitePostgreSQL, MySQL, SQLite
DSL de queriesSim (Rust puro)Não (SQL puro)
Pool de conexõesVia r2d2 ou deadpoolIntegrado (PgPool)
Curva de aprendizadoAlta (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:

  1. Migrations: As migrations são SQL puro em ambos — copie os arquivos .sql diretamente
  2. Models: Troque #[derive(Queryable)] por #[derive(FromRow)]
  3. Queries: Converta as queries do DSL Diesel para SQL puro nas macros query!/query_as!
  4. Pool: Substitua r2d2::Pool por PgPool do SQLx
  5. Schema: Remova o schema.rs — não é mais necessário
  6. Prepare offline: Execute cargo sqlx prepare para 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.

Veja Também