Introdução
O Diesel é o ORM (Object-Relational Mapping) mais maduro e battle-tested do ecossistema Rust. Ele adota uma filosofia de “zero-cost abstractions” para acesso a banco de dados, onde queries são construídas usando uma DSL (Domain Specific Language) fortemente tipada que é verificada em tempo de compilação.
Diferente de ORMs tradicionais em outras linguagens, o Diesel não usa reflection, strings mágicas ou geração de código em runtime. Cada query é uma expressão de tipos que o compilador Rust valida completamente, garantindo que erros de SQL – colunas inexistentes, tipos incompatíveis, joins inválidos – sejam capturados antes do código rodar.
Por que usar o Diesel?
- Type safety completo: queries são expressões de tipos verificadas pelo compilador
- Zero-cost abstractions: o Diesel gera SQL otimizado sem overhead
- Schema DSL: represente o esquema do banco como código Rust
- Migrations: sistema de migrations integrado via CLI
- Query builder poderoso: queries complexas com joins, subqueries e aggregations
- Extensível: suporte a tipos customizados, funções SQL e extensões do banco
- Maduro: usado em produção por anos, incluindo no crates.io
Instalação
Adicione o Diesel ao seu Cargo.toml:
[dependencies]
diesel = { version = "2.2", features = ["postgres"] }
dotenvy = "0.15"
Instale a CLI do Diesel:
# Com suporte a PostgreSQL
cargo install diesel_cli --no-default-features --features postgres
# Com suporte a SQLite
cargo install diesel_cli --no-default-features --features sqlite
# Com suporte a múltiplos bancos
cargo install diesel_cli --no-default-features --features "postgres sqlite mysql"
Configure a conexão com o banco:
# Criar arquivo .env
echo 'DATABASE_URL=postgres://usuario:senha@localhost:5432/meu_banco' > .env
# Inicializar o projeto Diesel (cria diesel.toml e pasta migrations/)
diesel setup
Features disponíveis:
[dependencies]
diesel = { version = "2.2", features = [
"postgres", # Suporte a PostgreSQL
# "mysql", # Suporte a MySQL
# "sqlite", # Suporte a SQLite
"chrono", # Tipos de data/hora
"serde_json", # Tipos JSON/JSONB
"uuid", # Tipo UUID
"r2d2", # Connection pooling
] }
Uso Básico
Criando migrations
# Criar uma nova migration
diesel migration generate criar_tabela_posts
# Isso cria dois arquivos:
# migrations/20260227120000_criar_tabela_posts/up.sql
# migrations/20260227120000_criar_tabela_posts/down.sql
Escreva as migrations:
-- up.sql
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
titulo VARCHAR(200) NOT NULL,
corpo TEXT NOT NULL,
publicado BOOLEAN NOT NULL DEFAULT false,
criado_em TIMESTAMP NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE comentarios (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
autor VARCHAR(100) NOT NULL,
texto TEXT NOT NULL,
criado_em TIMESTAMP NOT NULL DEFAULT NOW()
);
-- down.sql
DROP TABLE comentarios;
DROP TABLE posts;
Execute as migrations:
# Executar migrations pendentes
diesel migration run
# Reverter última migration
diesel migration revert
# Refazer última migration (revert + run)
diesel migration redo
Após executar, o Diesel gera automaticamente o arquivo src/schema.rs:
// src/schema.rs (gerado automaticamente)
diesel::table! {
posts (id) {
id -> Int4,
titulo -> Varchar,
corpo -> Text,
publicado -> Bool,
criado_em -> Timestamp,
atualizado_em -> Timestamp,
}
}
diesel::table! {
comentarios (id) {
id -> Int4,
post_id -> Int4,
autor -> Varchar,
texto -> Text,
criado_em -> Timestamp,
}
}
diesel::joinable!(comentarios -> posts (post_id));
diesel::allow_tables_to_appear_in_same_query!(posts, comentarios);
Definindo modelos
use diesel::prelude::*;
use chrono::NaiveDateTime;
use crate::schema::{posts, comentarios};
// Modelo para leitura (SELECT)
#[derive(Debug, Queryable, Selectable)]
#[diesel(table_name = posts)]
pub struct Post {
pub id: i32,
pub titulo: String,
pub corpo: String,
pub publicado: bool,
pub criado_em: NaiveDateTime,
pub atualizado_em: NaiveDateTime,
}
// Modelo para inserção (INSERT)
#[derive(Debug, Insertable)]
#[diesel(table_name = posts)]
pub struct NovoPost<'a> {
pub titulo: &'a str,
pub corpo: &'a str,
pub publicado: bool,
}
// Modelo para atualização (UPDATE)
#[derive(Debug, AsChangeset)]
#[diesel(table_name = posts)]
pub struct AtualizarPost<'a> {
pub titulo: Option<&'a str>,
pub corpo: Option<&'a str>,
pub publicado: Option<bool>,
}
// Modelo de comentário
#[derive(Debug, Queryable, Selectable)]
#[diesel(table_name = comentarios)]
#[diesel(belongs_to(Post))]
pub struct Comentario {
pub id: i32,
pub post_id: i32,
pub autor: String,
pub texto: String,
pub criado_em: NaiveDateTime,
}
#[derive(Debug, Insertable)]
#[diesel(table_name = comentarios)]
pub struct NovoComentario<'a> {
pub post_id: i32,
pub autor: &'a str,
pub texto: &'a str,
}
Operações CRUD básicas
use diesel::prelude::*;
use diesel::PgConnection;
// Estabelecer conexão
fn criar_conexao() -> PgConnection {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL deve estar definida");
PgConnection::establish(&database_url)
.expect("Erro ao conectar ao banco")
}
// CREATE
fn criar_post(conn: &mut PgConnection, titulo: &str, corpo: &str) -> Post {
use crate::schema::posts;
let novo_post = NovoPost {
titulo,
corpo,
publicado: false,
};
diesel::insert_into(posts::table)
.values(&novo_post)
.returning(Post::as_returning())
.get_result(conn)
.expect("Erro ao criar post")
}
// READ - Listar todos
fn listar_posts(conn: &mut PgConnection) -> Vec<Post> {
use crate::schema::posts::dsl::*;
posts
.filter(publicado.eq(true))
.order(criado_em.desc())
.limit(10)
.select(Post::as_select())
.load(conn)
.expect("Erro ao listar posts")
}
// READ - Buscar por ID
fn obter_post(conn: &mut PgConnection, post_id: i32) -> Option<Post> {
use crate::schema::posts::dsl::*;
posts
.find(post_id)
.select(Post::as_select())
.first(conn)
.optional()
.expect("Erro ao buscar post")
}
// UPDATE
fn atualizar_post(
conn: &mut PgConnection,
post_id: i32,
dados: &AtualizarPost,
) -> Option<Post> {
use crate::schema::posts::dsl::*;
diesel::update(posts.find(post_id))
.set(dados)
.returning(Post::as_returning())
.get_result(conn)
.optional()
.expect("Erro ao atualizar post")
}
// DELETE
fn deletar_post(conn: &mut PgConnection, post_id: i32) -> bool {
use crate::schema::posts::dsl::*;
let linhas = diesel::delete(posts.find(post_id))
.execute(conn)
.expect("Erro ao deletar post");
linhas > 0
}
// Publicar post
fn publicar_post(conn: &mut PgConnection, post_id: i32) -> Option<Post> {
use crate::schema::posts::dsl::*;
diesel::update(posts.find(post_id))
.set(publicado.eq(true))
.returning(Post::as_returning())
.get_result(conn)
.optional()
.expect("Erro ao publicar post")
}
Recursos Avançados
Queries complexas com filtros
use diesel::prelude::*;
fn buscar_posts(
conn: &mut PgConnection,
termo: Option<&str>,
apenas_publicados: bool,
limite: i64,
pagina: i64,
) -> Vec<Post> {
use crate::schema::posts::dsl::*;
let mut query = posts.into_boxed();
if apenas_publicados {
query = query.filter(publicado.eq(true));
}
if let Some(termo) = termo {
let padrao = format!("%{}%", termo);
query = query.filter(
titulo.ilike(padrao.clone())
.or(corpo.ilike(padrao))
);
}
query
.order(criado_em.desc())
.limit(limite)
.offset((pagina - 1) * limite)
.select(Post::as_select())
.load(conn)
.expect("Erro ao buscar posts")
}
Joins
use diesel::prelude::*;
// Post com contagem de comentários
#[derive(Debug, Queryable)]
struct PostComContagem {
post: Post,
total_comentarios: i64,
}
fn posts_com_comentarios(conn: &mut PgConnection) -> Vec<(Post, Vec<Comentario>)> {
use crate::schema::{posts, comentarios};
let todos_posts = posts::table
.filter(posts::publicado.eq(true))
.select(Post::as_select())
.load(conn)
.expect("Erro ao carregar posts");
let todos_comentarios = Comentario::belonging_to(&todos_posts)
.select(Comentario::as_select())
.load(conn)
.expect("Erro ao carregar comentários");
let comentarios_por_post = todos_comentarios
.grouped_by(&todos_posts);
todos_posts
.into_iter()
.zip(comentarios_por_post)
.collect()
}
// Join explícito
fn posts_com_autores(conn: &mut PgConnection) -> Vec<(Post, Comentario)> {
use crate::schema::{posts, comentarios};
posts::table
.inner_join(comentarios::table)
.filter(posts::publicado.eq(true))
.select((Post::as_select(), Comentario::as_select()))
.load(conn)
.expect("Erro no join")
}
Aggregations
use diesel::prelude::*;
use diesel::dsl::count_star;
fn estatisticas_posts(conn: &mut PgConnection) {
use crate::schema::posts::dsl::*;
// Contagem total
let total: i64 = posts
.count()
.get_result(conn)
.expect("Erro ao contar");
println!("Total de posts: {}", total);
// Contagem por status
let publicados: i64 = posts
.filter(publicado.eq(true))
.count()
.get_result(conn)
.expect("Erro ao contar publicados");
let rascunhos: i64 = posts
.filter(publicado.eq(false))
.count()
.get_result(conn)
.expect("Erro ao contar rascunhos");
println!("Publicados: {}, Rascunhos: {}", publicados, rascunhos);
}
fn contagem_comentarios_por_post(
conn: &mut PgConnection,
) -> Vec<(i32, String, i64)> {
use crate::schema::{posts, comentarios};
posts::table
.left_join(comentarios::table)
.group_by((posts::id, posts::titulo))
.select((
posts::id,
posts::titulo,
diesel::dsl::count(comentarios::id.nullable()),
))
.order(diesel::dsl::count(comentarios::id.nullable()).desc())
.load(conn)
.expect("Erro na agregação")
}
Inserção em batch
use diesel::prelude::*;
fn inserir_varios_posts(conn: &mut PgConnection, novos: &[NovoPost]) -> Vec<Post> {
use crate::schema::posts;
diesel::insert_into(posts::table)
.values(novos)
.returning(Post::as_returning())
.get_results(conn)
.expect("Erro ao inserir em batch")
}
// Upsert (INSERT ... ON CONFLICT)
fn upsert_post(conn: &mut PgConnection, novo: &NovoPost) -> Post {
use crate::schema::posts;
diesel::insert_into(posts::table)
.values(novo)
.on_conflict(posts::titulo)
.do_update()
.set(posts::corpo.eq(novo.corpo))
.returning(Post::as_returning())
.get_result(conn)
.expect("Erro no upsert")
}
Connection pooling com r2d2
use diesel::r2d2::{self, ConnectionManager};
use diesel::PgConnection;
type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
fn criar_pool() -> DbPool {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL deve estar definida");
let manager = ConnectionManager::<PgConnection>::new(database_url);
r2d2::Pool::builder()
.max_size(15)
.min_idle(Some(5))
.build(manager)
.expect("Falha ao criar pool de conexões")
}
// Usando em um handler web
fn listar_posts_handler(pool: &DbPool) -> Vec<Post> {
use crate::schema::posts::dsl::*;
let mut conn = pool.get().expect("Falha ao obter conexão do pool");
posts
.filter(publicado.eq(true))
.select(Post::as_select())
.load(&mut conn)
.expect("Erro ao listar posts")
}
Transações
use diesel::prelude::*;
fn criar_post_com_comentario(
conn: &mut PgConnection,
titulo: &str,
corpo: &str,
autor_comentario: &str,
texto_comentario: &str,
) -> Result<(Post, Comentario), diesel::result::Error> {
use crate::schema::{posts, comentarios};
conn.transaction(|conn| {
// Criar o post
let post = diesel::insert_into(posts::table)
.values(&NovoPost {
titulo,
corpo,
publicado: true,
})
.returning(Post::as_returning())
.get_result(conn)?;
// Criar o comentário inicial
let comentario = diesel::insert_into(comentarios::table)
.values(&NovoComentario {
post_id: post.id,
autor: autor_comentario,
texto: texto_comentario,
})
.returning(Comentario::as_returning())
.get_result(conn)?;
Ok((post, comentario))
})
}
SQL customizado
use diesel::prelude::*;
use diesel::sql_query;
use diesel::sql_types::{Integer, Text, Bool};
#[derive(Debug, QueryableByName)]
struct PostResumo {
#[diesel(sql_type = Integer)]
id: i32,
#[diesel(sql_type = Text)]
titulo: String,
#[diesel(sql_type = diesel::sql_types::BigInt)]
total_comentarios: i64,
}
fn posts_populares(conn: &mut PgConnection) -> Vec<PostResumo> {
sql_query(
r#"
SELECT p.id, p.titulo, COUNT(c.id) as total_comentarios
FROM posts p
LEFT JOIN comentarios c ON c.post_id = p.id
WHERE p.publicado = true
GROUP BY p.id, p.titulo
HAVING COUNT(c.id) > 0
ORDER BY total_comentarios DESC
LIMIT 10
"#
)
.load(conn)
.expect("Erro na query SQL customizada")
}
Boas Práticas
1. Organize modelos e queries
src/
schema.rs # Gerado pelo Diesel
models/
mod.rs
post.rs # Modelos e queries de Post
comentario.rs # Modelos e queries de Comentario
db.rs # Pool de conexões
main.rs
2. Use AsChangeset para atualizações parciais
#[derive(Debug, AsChangeset)]
#[diesel(table_name = posts)]
struct AtualizarPost<'a> {
// Campos Option<T>: None = não alterar, Some(v) = alterar para v
pub titulo: Option<&'a str>,
pub corpo: Option<&'a str>,
pub publicado: Option<bool>,
}
fn atualizar_titulo(conn: &mut PgConnection, id: i32, novo_titulo: &str) {
use crate::schema::posts::dsl::*;
let mudancas = AtualizarPost {
titulo: Some(novo_titulo),
corpo: None, // Não altera
publicado: None, // Não altera
};
diesel::update(posts.find(id))
.set(&mudancas)
.execute(conn)
.expect("Erro ao atualizar");
}
3. Trate erros adequadamente
use diesel::result::{Error as DieselError, DatabaseErrorKind};
fn criar_post_seguro(
conn: &mut PgConnection,
novo: &NovoPost,
) -> Result<Post, String> {
use crate::schema::posts;
diesel::insert_into(posts::table)
.values(novo)
.returning(Post::as_returning())
.get_result(conn)
.map_err(|err| match err {
DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, info) => {
format!("Duplicado: {}", info.message())
}
DieselError::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, info) => {
format!("Referência inválida: {}", info.message())
}
DieselError::NotFound => "Registro não encontrado".to_string(),
_ => format!("Erro no banco: {}", err),
})
}
4. Mantenha o schema.rs atualizado
# Regenerar schema.rs a partir do banco
diesel print-schema > src/schema.rs
# Configurar diesel.toml para gerar automaticamente
# diesel.toml:
# [print_schema]
# file = "src/schema.rs"
5. Escreva testes com banco de dados de teste
#[cfg(test)]
mod tests {
use super::*;
use diesel::Connection;
fn setup_teste() -> PgConnection {
let url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://teste:teste@localhost/teste".to_string());
let mut conn = PgConnection::establish(&url).unwrap();
conn.begin_test_transaction().unwrap();
conn
}
#[test]
fn test_criar_e_listar_posts() {
let mut conn = setup_teste();
let post = criar_post(&mut conn, "Teste", "Corpo do teste");
assert_eq!(post.titulo, "Teste");
assert!(!post.publicado);
publicar_post(&mut conn, post.id);
let posts = listar_posts(&mut conn);
assert!(posts.iter().any(|p| p.id == post.id));
}
#[test]
fn test_deletar_post() {
let mut conn = setup_teste();
let post = criar_post(&mut conn, "Para deletar", "Corpo");
assert!(deletar_post(&mut conn, post.id));
assert!(!deletar_post(&mut conn, post.id)); // Já deletado
}
}
Exemplos Práticos
Aplicação de blog completa
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use chrono::NaiveDateTime;
mod schema {
diesel::table! {
posts (id) {
id -> Int4,
titulo -> Varchar,
slug -> Varchar,
corpo -> Text,
resumo -> Nullable<Text>,
publicado -> Bool,
criado_em -> Timestamp,
atualizado_em -> Timestamp,
}
}
diesel::table! {
tags (id) {
id -> Int4,
nome -> Varchar,
}
}
diesel::table! {
post_tags (post_id, tag_id) {
post_id -> Int4,
tag_id -> Int4,
}
}
diesel::joinable!(post_tags -> posts (post_id));
diesel::joinable!(post_tags -> tags (tag_id));
diesel::allow_tables_to_appear_in_same_query!(posts, tags, post_tags);
}
use schema::*;
// === Modelos ===
#[derive(Debug, Queryable, Selectable, Identifiable)]
#[diesel(table_name = posts)]
struct Post {
id: i32,
titulo: String,
slug: String,
corpo: String,
resumo: Option<String>,
publicado: bool,
criado_em: NaiveDateTime,
atualizado_em: NaiveDateTime,
}
#[derive(Debug, Insertable)]
#[diesel(table_name = posts)]
struct NovoPost<'a> {
titulo: &'a str,
slug: &'a str,
corpo: &'a str,
resumo: Option<&'a str>,
}
#[derive(Debug, Queryable, Selectable, Identifiable)]
#[diesel(table_name = tags)]
struct Tag {
id: i32,
nome: String,
}
#[derive(Debug, Insertable)]
#[diesel(table_name = post_tags)]
struct PostTag {
post_id: i32,
tag_id: i32,
}
// === Repositório de Blog ===
type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
struct BlogRepo {
pool: DbPool,
}
impl BlogRepo {
fn new(database_url: &str) -> Self {
let manager = ConnectionManager::<PgConnection>::new(database_url);
let pool = r2d2::Pool::builder()
.max_size(10)
.build(manager)
.expect("Falha ao criar pool");
Self { pool }
}
fn conn(&self) -> r2d2::PooledConnection<ConnectionManager<PgConnection>> {
self.pool.get().expect("Falha ao obter conexão")
}
fn criar_post(&self, titulo: &str, corpo: &str, nomes_tags: &[&str]) -> Post {
let mut conn = self.conn();
conn.transaction(|conn| {
// Gerar slug a partir do título
let slug = titulo
.to_lowercase()
.replace(' ', "-")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-')
.collect::<String>();
let resumo = if corpo.len() > 200 {
Some(&corpo[..200])
} else {
None
};
// Criar o post
let post = diesel::insert_into(posts::table)
.values(&NovoPost {
titulo,
slug: &slug,
corpo,
resumo,
})
.returning(Post::as_returning())
.get_result(conn)?;
// Criar tags e associações
for nome_tag in nomes_tags {
// Upsert da tag
let tag: Tag = diesel::insert_into(tags::table)
.values(tags::nome.eq(nome_tag))
.on_conflict(tags::nome)
.do_update()
.set(tags::nome.eq(nome_tag))
.returning(Tag::as_returning())
.get_result(conn)?;
// Associar tag ao post
diesel::insert_into(post_tags::table)
.values(&PostTag {
post_id: post.id,
tag_id: tag.id,
})
.on_conflict_do_nothing()
.execute(conn)?;
}
Ok(post)
})
.expect("Erro ao criar post com tags")
}
fn listar_publicados(&self, pagina: i64, por_pagina: i64) -> Vec<Post> {
let mut conn = self.conn();
posts::table
.filter(posts::publicado.eq(true))
.order(posts::criado_em.desc())
.limit(por_pagina)
.offset((pagina - 1) * por_pagina)
.select(Post::as_select())
.load(&mut conn)
.expect("Erro ao listar posts")
}
fn obter_por_slug(&self, slug_busca: &str) -> Option<Post> {
let mut conn = self.conn();
posts::table
.filter(posts::slug.eq(slug_busca))
.select(Post::as_select())
.first(&mut conn)
.optional()
.expect("Erro ao buscar post")
}
fn tags_do_post(&self, post_id: i32) -> Vec<Tag> {
let mut conn = self.conn();
tags::table
.inner_join(post_tags::table)
.filter(post_tags::post_id.eq(post_id))
.select(Tag::as_select())
.load(&mut conn)
.expect("Erro ao buscar tags")
}
fn buscar(&self, termo: &str) -> Vec<Post> {
let mut conn = self.conn();
let padrao = format!("%{}%", termo);
posts::table
.filter(
posts::publicado.eq(true).and(
posts::titulo.ilike(&padrao)
.or(posts::corpo.ilike(&padrao)),
),
)
.order(posts::criado_em.desc())
.select(Post::as_select())
.load(&mut conn)
.expect("Erro ao buscar")
}
}
fn main() {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL necessária");
let blog = BlogRepo::new(&database_url);
// Criar um post com tags
let post = blog.criar_post(
"Aprendendo Diesel em Rust",
"O Diesel é o ORM mais maduro do ecossistema Rust...",
&["rust", "diesel", "tutorial"],
);
println!("Post criado: {} (slug: {})", post.titulo, post.slug);
// Listar tags
let tags = blog.tags_do_post(post.id);
println!("Tags: {:?}", tags.iter().map(|t| &t.nome).collect::<Vec<_>>());
// Buscar posts
let resultados = blog.buscar("Diesel");
println!("Encontrados: {} posts", resultados.len());
}
Comparação com Alternativas
| Característica | Diesel | SQLx | SeaORM | Rusqlite |
|---|---|---|---|---|
| Abordagem | ORM com DSL tipada | SQL puro verificado | ORM ActiveModel | Bindings SQLite |
| Async | Não (síncrono) | Sim (nativo) | Sim | Não |
| Compile-time check | DSL tipada | query!() macro | Não | Não |
| Migrations | diesel_cli | sqlx-cli | sea-orm-cli | Manual |
| Query builder | DSL fortemente tipada | QueryBuilder | Entidades/ActiveModel | SQL puro |
| Pool | r2d2 | Embutido | Embutido | N/A |
| Curva | Média-alta | Baixa-média | Média | Baixa |
- Diesel vs SQLx: Diesel oferece mais segurança de tipos no query building, mas é síncrono. SQLx usa SQL puro e é assíncrono. Escolha Diesel se valoriza abstração e type-safety no query builder. Escolha SQLx se precisa de async e prefere SQL puro.
- Diesel vs SeaORM: SeaORM é construído sobre SQLx e oferece um modelo mais parecido com ORMs tradicionais. Diesel é mais maduro e com melhor type-safety.
- Para projetos novos com Tokio/async, considere SQLx. Para projetos que valorizam abstrações de ORM fortes, Diesel é excelente.
Conclusão
O Diesel é uma escolha sólida para projetos Rust que precisam de acesso seguro e eficiente a bancos de dados. Sua DSL de queries fortemente tipada, sistema de migrations integrado e verificação em compile-time eliminam classes inteiras de bugs comuns em aplicações que acessam bancos de dados.
Apesar de ser síncrono, o Diesel compensa com uma ergonomia excelente para queries complexas, joins e agregações. Para projetos que não precisam de async no acesso ao banco (ou que usam spawn_blocking), o Diesel oferece uma experiência de desenvolvimento excepcional.
Próximos passos
- Explore o SQLx como alternativa assíncrona
- Use r2d2 para connection pooling em produção
- Aprenda sobre diesel_async para integração com Tokio
- Configure Tracing para monitorar performance de queries