Diesel

Diesel #

Diesel adalah ORM (Object-Relational Mapper) Rust yang mengutamakan keamanan tipe di atas segalanya. Berbeda dari sqlx yang memeriksa query SQL mentah saat compile time, Diesel menyediakan query builder berbasis DSL Rust — kamu membangun query menggunakan fungsi dan method Rust, bukan string SQL. Ini berarti query yang salah (kolom tidak ada, tipe tidak cocok, join yang salah) menjadi error kompilasi, bukan error runtime. Tradeoff-nya: kurva belajar lebih curam, dan beberapa query kompleks lebih mudah ditulis dalam SQL mentah. Diesel menggunakan synchronous connection (tidak async) secara default — gunakan diesel-async untuk kebutuhan async. Artikel ini membahas Diesel 2.x dengan PostgreSQL.

Diesel vs sqlx — Pilih yang Tepat #

flowchart TD
    Q{Prioritas utama?}
    Q --> A["Type-safe query builder\nTidak mau tulis SQL\nORM relationship lengkap"]
    Q --> B["Tulis SQL langsung\nAsync native\nFleksibel untuk query kompleks"]
    Q --> C["ORM dengan async\nType-safe DAN async"]

    A --> D["Diesel\nDSL Rust yang komprehensif\nMigrasi built-in\nSync (atau diesel-async)"]
    B --> E["sqlx\nAsync native\nQuery check compile-time\nSQL murni"]
    C --> F["SeaORM\nORM async berbasis sqlx\nRelationship kaya"]
AspekDieselsqlx
PendekatanQuery builder DSLSQL murni
AsyncVia diesel-asyncNative async
Type safetyDSL RustCompile-time SQL check
MigrasiBuilt-in + diesel-clisqlx-cli
Learning curveLebih curamLebih landai
Query kompleksSulit (butuh SQL raw)Mudah (SQL langsung)
Cocok untukCRUD standar, relasi sederhanaQuery beragam, kontrol penuh

Instalasi dan Setup #

[dependencies]
diesel = { version = "2.1", features = ["postgres", "r2d2", "chrono"] }
dotenvy = "0.15"
serde = { version = "1", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies]
diesel_migrations = "2.1"

Install diesel-cli:

# Hanya untuk PostgreSQL
cargo install diesel_cli --no-default-features --features postgres

# Setup diesel (buat .env, migrations/, src/schema.rs)
echo DATABASE_URL=postgres://user:password@localhost/mydb > .env
diesel setup

Migrasi #

# Buat file migrasi baru
diesel migration generate buat_tabel_pengguna

# Jalankan migrasi
diesel migration run

# Undo migrasi terakhir
diesel migration revert

# Redo (revert + run)
diesel migration redo

# Cek status semua migrasi
diesel migration list

File migrasi yang dihasilkan:

-- migrations/2024-08-24-000001_buat_tabel_pengguna/up.sql
CREATE TABLE pengguna (
    id          BIGSERIAL PRIMARY KEY,
    nama        TEXT NOT NULL,
    email       TEXT NOT NULL UNIQUE,
    password    TEXT NOT NULL,
    peran       TEXT NOT NULL DEFAULT 'user',
    aktif       BOOLEAN NOT NULL DEFAULT TRUE,
    dibuat_pada TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    diperbarui  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_pengguna_email ON pengguna(email);
CREATE INDEX idx_pengguna_aktif ON pengguna(aktif);
-- migrations/2024-08-24-000001_buat_tabel_pengguna/down.sql
DROP TABLE pengguna;

Setelah diesel migration run, Diesel men-generate src/schema.rs secara otomatis:

// src/schema.rs — dihasilkan otomatis, jangan edit manual
diesel::table! {
    pengguna (id) {
        id -> Int8,
        nama -> Text,
        email -> Text,
        password -> Text,
        peran -> Text,
        aktif -> Bool,
        dibuat_pada -> Timestamptz,
        diperbarui -> Timestamptz,
    }
}

Model dan Struct #

// src/models.rs
use crate::schema::pengguna;
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};

// Struct untuk membaca dari database
#[derive(Debug, Queryable, Selectable, Serialize, Clone)]
#[diesel(table_name = pengguna)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Pengguna {
    pub id: i64,
    pub nama: String,
    pub email: String,
    #[serde(skip_serializing)]
    pub password: String,
    pub peran: String,
    pub aktif: bool,
    pub dibuat_pada: DateTime<Utc>,
    pub diperbarui: DateTime<Utc>,
}

// Struct untuk INSERT — tanpa field yang di-generate database
#[derive(Debug, Insertable, Deserialize)]
#[diesel(table_name = pengguna)]
pub struct PenggunaBaru {
    pub nama: String,
    pub email: String,
    pub password: String,
    pub peran: Option<String>,
}

// Struct untuk UPDATE — semua field opsional
#[derive(Debug, AsChangeset, Deserialize)]
#[diesel(table_name = pengguna)]
pub struct PembaruanPengguna {
    pub nama: Option<String>,
    pub email: Option<String>,
    pub aktif: Option<bool>,
}

Connection dan Pool #

// src/database.rs
use diesel::pg::PgConnection;
use diesel::r2d2::{self, ConnectionManager};
use std::env;

pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub type PooledConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>;

pub fn buat_pool() -> Pool {
    let url = env::var("DATABASE_URL").expect("DATABASE_URL harus diset");
    let manager = ConnectionManager::<PgConnection>::new(&url);

    r2d2::Pool::builder()
        .max_size(10)
        .min_idle(Some(2))
        .connection_timeout(std::time::Duration::from_secs(5))
        .build(manager)
        .expect("Gagal membuat connection pool")
}

// Koneksi tunggal untuk testing atau CLI tool
pub fn buat_koneksi() -> PgConnection {
    let url = env::var("DATABASE_URL").expect("DATABASE_URL harus diset");
    PgConnection::establish(&url).expect("Gagal terhubung ke database")
}

CRUD Operasi #

Create — INSERT #

use diesel::prelude::*;
use crate::schema::pengguna;
use crate::models::{Pengguna, PenggunaBaru};

pub fn buat_pengguna(
    conn: &mut PgConnection,
    input: &PenggunaBaru,
) -> QueryResult<Pengguna> {
    diesel::insert_into(pengguna::table)
        .values(input)
        .returning(Pengguna::as_returning())  // return baris yang diinsert
        .get_result(conn)
}

// Insert banyak sekaligus
pub fn buat_banyak_pengguna(
    conn: &mut PgConnection,
    input_list: &[PenggunaBaru],
) -> QueryResult<Vec<Pengguna>> {
    diesel::insert_into(pengguna::table)
        .values(input_list)
        .returning(Pengguna::as_returning())
        .get_results(conn)
}

// Insert atau update (UPSERT)
pub fn upsert_pengguna(
    conn: &mut PgConnection,
    input: &PenggunaBaru,
) -> QueryResult<Pengguna> {
    use diesel::pg::upsert::excluded;

    diesel::insert_into(pengguna::table)
        .values(input)
        .on_conflict(pengguna::email)
        .do_update()
        .set((
            pengguna::nama.eq(excluded(pengguna::nama)),
            pengguna::diperbarui.eq(chrono::Utc::now()),
        ))
        .returning(Pengguna::as_returning())
        .get_result(conn)
}

Read — SELECT #

use diesel::prelude::*;
use crate::schema::pengguna;
use crate::models::Pengguna;

// Ambil semua
pub fn ambil_semua_pengguna(conn: &mut PgConnection) -> QueryResult<Vec<Pengguna>> {
    pengguna::table
        .select(Pengguna::as_select())
        .order(pengguna::dibuat_pada.desc())
        .load(conn)
}

// Ambil berdasarkan ID
pub fn ambil_pengguna_by_id(conn: &mut PgConnection, id: i64) -> QueryResult<Pengguna> {
    pengguna::table
        .find(id)
        .select(Pengguna::as_select())
        .first(conn)
}

// Dengan filter
pub fn cari_pengguna(
    conn: &mut PgConnection,
    kata_kunci: &str,
    hanya_aktif: bool,
    batas: i64,
    offset: i64,
) -> QueryResult<Vec<Pengguna>> {
    let kata = format!("%{}%", kata_kunci);

    pengguna::table
        .select(Pengguna::as_select())
        .filter(pengguna::aktif.eq(hanya_aktif))
        .filter(
            pengguna::nama.ilike(&kata)
                .or(pengguna::email.ilike(&kata))
        )
        .order(pengguna::nama.asc())
        .limit(batas)
        .offset(offset)
        .load(conn)
}

// Count
pub fn hitung_pengguna(conn: &mut PgConnection) -> QueryResult<i64> {
    pengguna::table.count().get_result(conn)
}

// Cek keberadaan
pub fn email_ada(conn: &mut PgConnection, email: &str) -> QueryResult<bool> {
    use diesel::dsl::exists;
    diesel::select(exists(
        pengguna::table.filter(pengguna::email.eq(email))
    ))
    .get_result(conn)
}

Update #

use diesel::prelude::*;
use crate::schema::pengguna;
use crate::models::{Pengguna, PembaruanPengguna};

pub fn perbarui_pengguna(
    conn: &mut PgConnection,
    id: i64,
    perubahan: &PembaruanPengguna,
) -> QueryResult<Pengguna> {
    diesel::update(pengguna::table.find(id))
        .set(perubahan)
        .returning(Pengguna::as_returning())
        .get_result(conn)
}

// Update field spesifik
pub fn nonaktifkan_pengguna(
    conn: &mut PgConnection,
    id: i64,
) -> QueryResult<usize> {
    diesel::update(pengguna::table.find(id))
        .set((
            pengguna::aktif.eq(false),
            pengguna::diperbarui.eq(chrono::Utc::now()),
        ))
        .execute(conn)
}

// Update banyak baris
pub fn nonaktifkan_semua_pengguna_peran(
    conn: &mut PgConnection,
    peran: &str,
) -> QueryResult<usize> {
    diesel::update(pengguna::table.filter(pengguna::peran.eq(peran)))
        .set(pengguna::aktif.eq(false))
        .execute(conn)
}

Delete #

use diesel::prelude::*;
use crate::schema::pengguna;

pub fn hapus_pengguna(conn: &mut PgConnection, id: i64) -> QueryResult<usize> {
    diesel::delete(pengguna::table.find(id)).execute(conn)
}

// Soft delete (update aktif = false, tidak benar-benar hapus)
pub fn soft_delete(conn: &mut PgConnection, id: i64) -> QueryResult<usize> {
    diesel::update(pengguna::table.find(id))
        .set(pengguna::aktif.eq(false))
        .execute(conn)
}

Query Lanjutan dan JOIN #

// src/schema.rs — tambahkan tabel artikel
diesel::table! {
    artikel (id) {
        id -> Int8,
        pengguna_id -> Int8,
        judul -> Text,
        konten -> Text,
        diterbitkan -> Bool,
        dibuat_pada -> Timestamptz,
    }
}

diesel::joinable!(artikel -> pengguna (pengguna_id));
diesel::allow_tables_to_appear_in_same_query!(pengguna, artikel);
use diesel::prelude::*;
use crate::schema::{artikel, pengguna};

#[derive(Debug, Queryable, Selectable)]
#[diesel(table_name = artikel)]
pub struct Artikel {
    pub id: i64,
    pub pengguna_id: i64,
    pub judul: String,
    pub konten: String,
    pub diterbitkan: bool,
    pub dibuat_pada: chrono::DateTime<chrono::Utc>,
}

// JOIN query
pub fn artikel_dengan_nama_penulis(
    conn: &mut PgConnection,
) -> QueryResult<Vec<(Artikel, String)>> {
    artikel::table
        .inner_join(pengguna::table)
        .select((Artikel::as_select(), pengguna::nama))
        .filter(artikel::diterbitkan.eq(true))
        .order(artikel::dibuat_pada.desc())
        .load::<(Artikel, String)>(conn)
}

// Artikel milik pengguna tertentu
pub fn artikel_pengguna(
    conn: &mut PgConnection,
    user_id: i64,
) -> QueryResult<Vec<Artikel>> {
    artikel::table
        .filter(artikel::pengguna_id.eq(user_id))
        .select(Artikel::as_select())
        .order(artikel::dibuat_pada.desc())
        .load(conn)
}

Transaksi #

use diesel::prelude::*;
use diesel::Connection;

pub fn transfer_poin(
    conn: &mut PgConnection,
    dari_id: i64,
    ke_id: i64,
    jumlah: i32,
) -> QueryResult<()> {
    conn.transaction(|conn| {
        // Semua operasi dalam closure ini adalah satu transaksi
        // Jika ada yang gagal (return Err), seluruh transaksi di-rollback otomatis

        // Kurangi poin pengirim
        let baris = diesel::update(pengguna::table.find(dari_id))
            .set(pengguna::diperbarui.eq(chrono::Utc::now()))
            .execute(conn)?;

        if baris == 0 {
            return Err(diesel::result::Error::NotFound);
        }

        // Tambah poin penerima
        diesel::update(pengguna::table.find(ke_id))
            .set(pengguna::diperbarui.eq(chrono::Utc::now()))
            .execute(conn)?;

        Ok(())
    })
}

// Transaksi dengan savepoint (nested transaction)
pub fn operasi_batch(
    conn: &mut PgConnection,
    operasi: Vec<PenggunaBaru>,
) -> QueryResult<Vec<Result<Pengguna, diesel::result::Error>>> {
    let mut hasil = Vec::new();

    for input in &operasi {
        // Savepoint per operasi — rollback sebagian jika ada yang gagal
        let result = conn.transaction::<Pengguna, diesel::result::Error, _>(|conn| {
            diesel::insert_into(pengguna::table)
                .values(input)
                .returning(Pengguna::as_returning())
                .get_result(conn)
        });
        hasil.push(result);
    }

    Ok(hasil)
}

Raw SQL — Ketika DSL Tidak Cukup #

Untuk query yang terlalu kompleks untuk DSL Diesel, gunakan SQL mentah:

use diesel::prelude::*;
use diesel::sql_query;
use diesel::sql_types::{BigInt, Text, Bool};

#[derive(Debug, QueryableByName)]
pub struct HasilAnalitik {
    #[diesel(sql_type = Text)]
    pub peran: String,
    #[diesel(sql_type = BigInt)]
    pub jumlah: i64,
    #[diesel(sql_type = BigInt)]
    pub aktif: i64,
}

pub fn statistik_per_peran(conn: &mut PgConnection) -> QueryResult<Vec<HasilAnalitik>> {
    sql_query(
        "SELECT peran, COUNT(*) as jumlah, SUM(CASE WHEN aktif THEN 1 ELSE 0 END) as aktif
         FROM pengguna
         GROUP BY peran
         ORDER BY jumlah DESC"
    )
    .load(conn)
}

// Raw SQL dengan parameter binding
pub fn cari_pengguna_raw(
    conn: &mut PgConnection,
    kata: &str,
) -> QueryResult<Vec<Pengguna>> {
    use diesel::sql_types::Text;

    diesel::sql_query(
        "SELECT * FROM pengguna WHERE nama ILIKE $1 OR email ILIKE $1 LIMIT 20"
    )
    .bind::<Text, _>(format!("%{}%", kata))
    .load(conn)
}

Integrasi dengan Axum (Async) #

Karena Diesel synchronous, bungkus dalam spawn_blocking untuk Axum:

use axum::{extract::State, http::StatusCode, response::Json};
use std::sync::Arc;

type DbPool = Arc<crate::database::Pool>;

pub async fn handler_daftar_pengguna(
    State(pool): State<DbPool>,
) -> Result<Json<Vec<Pengguna>>, StatusCode> {
    tokio::task::spawn_blocking(move || {
        let mut conn = pool.get()
            .map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
        ambil_semua_pengguna(&mut conn)
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
    })
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    .map(Json)
}

Ringkasan #

  • Diesel = query builder DSL, sqlx = SQL murni — pilih Diesel untuk kode yang lebih Rust-idiomatic dan lebih terproteksi dari SQL injection; pilih sqlx untuk query kompleks atau tim yang lebih familiar SQL.
  • diesel setup dan diesel migration run menghasilkan schema.rs otomatis — jangan edit schema.rs secara manual; ubah via migrasi dan regenerasi.
  • Tiga derive macro pentingQueryable untuk SELECT, Insertable untuk INSERT, AsChangeset untuk UPDATE. Selectable memungkinkan .select(Struct::as_select()).
  • get_result vs executeget_result untuk operasi yang mengembalikan baris (INSERT RETURNING, UPDATE RETURNING), execute untuk yang hanya mengembalikan jumlah baris terpengaruh.
  • Gunakan r2d2 untuk connection pool — Diesel tidak punya pool bawaan untuk async; r2d2 adalah pilihan standar untuk aplikasi sync/spawn_blocking.
  • conn.transaction(|conn| {...}) — closure yang return Err otomatis mem-rollback transaksi. Tidak perlu BEGIN/COMMIT/ROLLBACK manual.
  • .filter() bisa dikombinasikan — beberapa .filter() di-chain diinterpretasikan sebagai AND. Gunakan .or() untuk OR, .or_filter() untuk OR antar filter group.
  • sql_query + QueryableByName untuk raw SQL — ketika DSL tidak cukup, gunakan #[diesel(sql_type = ...)] pada field struct untuk mapping hasil.
  • Diesel synchronous → spawn_blocking untuk async runtime — gunakan tokio::task::spawn_blocking agar operasi database tidak memblokir event loop. Alternatif: gunakan crate diesel-async.

← Sebelumnya: Warp   Berikutnya: Selenium RS →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact