Oracle

Oracle #

Oracle Database adalah salah satu sistem database paling banyak digunakan di lingkungan enterprise besar — perbankan, telekomunikasi, dan pemerintahan. Mengaksesnya dari Rust punya tantangan tersendiri: tidak seperti MySQL atau PostgreSQL yang punya driver murni Rust, Oracle mengharuskan instalasi Oracle Instant Client (library C dari Oracle) sebagai dependensi sistem. Ini membuat setup lebih kompleks tapi tidak menghalangi penggunaan di produksi. Crate utama yang digunakan adalah oracle — binding Rust ke ODPI-C library. Artikel ini membahas setup lengkap dari nol, koneksi, query, perbedaan sintaks PL/SQL, dan pola umum untuk lingkungan enterprise.

Prasyarat: Oracle Instant Client #

Sebelum mengkompilasi kode Rust yang menggunakan oracle, kamu wajib menginstal Oracle Instant Client di sistem:

# Linux (Ubuntu/Debian) — download dari https://www.oracle.com/database/technologies/instant-client/downloads.html
# Extract ke /opt/oracle/instantclient_21_x

# Set environment variable
export LD_LIBRARY_PATH=/opt/oracle/instantclient_21_x:$LD_LIBRARY_PATH
export ORACLE_HOME=/opt/oracle/instantclient_21_x

# Untuk menghubungkan ke database tanpa tnsnames.ora
# bisa menggunakan Easy Connect syntax langsung di connection string
# macOS — download DMG dari Oracle, atau gunakan brew
brew tap InstantClientTap/instantclient
brew install instantclient-basic

export DYLD_LIBRARY_PATH=$(brew --prefix)/lib:$DYLD_LIBRARY_PATH
# Windows — download ZIP dari Oracle, extract ke C:\oracle\instantclient_21_x
# Tambahkan ke PATH
set PATH=C:\oracle\instantclient_21_x;%PATH%
Oracle Instant Client adalah library proprietary Oracle dan membutuhkan akun Oracle untuk diunduh. Ini adalah satu-satunya cara menghubungkan ke Oracle Database dari bahasa pemrograman apapun (termasuk Python, Java, Node.js). Pastikan versi Instant Client kompatibel dengan versi Oracle Database yang kamu gunakan.

Instalasi #

[dependencies]
oracle = "0.5"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
chrono = "0.4"
Crate oracle saat ini adalah synchronous — tidak ada dukungan async native. Untuk menggunakannya dalam aplikasi async, jalankan operasi database di tokio::task::spawn_blocking atau gunakan thread pool terpisah.

String Koneksi Oracle #

Oracle menggunakan beberapa format koneksi:

// Easy Connect (tanpa tnsnames.ora) — paling sederhana
"//hostname:port/service_name"
"//localhost:1521/XEPDB1"
"//prod-db.company.com:1521/ORCL"

// Easy Connect dengan opsi tambahan
"//hostname:port/service_name?connect_timeout=10"

// Dengan TNS alias (butuh tnsnames.ora dikonfigurasi)
"TNS_ALIAS"

// SID (untuk database lama)
"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=hostname)(PORT=1521))(CONNECT_DATA=(SID=orcl)))"

// Oracle Cloud (ATP/ADW) — menggunakan wallet
"(description=(retry_count=20)(retry_delay=3)(address=(protocol=tcps)(port=1522)(host=adb.region.oraclecloud.com))(connect_data=(service_name=xxxxx_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=yes)))"

Koneksi Dasar #

use oracle::{Connection, Error};

fn buat_koneksi(
    username: &str,
    password: &str,
    connect_string: &str,
) -> Result<Connection, Error> {
    Connection::connect(username, password, connect_string)
}

fn main() -> Result<(), Error> {
    // Koneksi ke Oracle XE lokal
    let conn = buat_koneksi(
        "system",
        "oracle",
        "//localhost:1521/XEPDB1",
    )?;

    println!("Terhubung ke Oracle Database");

    // Verifikasi koneksi
    let mut stmt = conn.statement("SELECT 'OK' FROM DUAL").build()?;
    let rows = stmt.query(&[])?;
    for row_result in rows {
        let row = row_result?;
        let status: String = row.get(0)?;
        println!("Status: {}", status);
    }

    Ok(())
}

Connection Pool #

Crate oracle tidak menyediakan connection pool bawaan. Gunakan crate r2d2 atau deadpool untuk pool:

[dependencies]
oracle = "0.5"
r2d2 = "0.8"
r2d2-oracle = "0.2"
tokio = { version = "1", features = ["full"] }
use oracle::Connection;
use r2d2::Pool;
use r2d2_oracle::OracleConnectionManager;
use std::sync::Arc;

type OraclePool = Pool<OracleConnectionManager>;

fn buat_pool(
    username: &str,
    password: &str,
    connect_string: &str,
    maks_koneksi: u32,
) -> Result<OraclePool, Box<dyn std::error::Error>> {
    let manager = OracleConnectionManager::new(username, password, connect_string);
    let pool = r2d2::Pool::builder()
        .max_size(maks_koneksi)
        .min_idle(Some(2))
        .connection_timeout(std::time::Duration::from_secs(10))
        .build(manager)?;
    Ok(pool)
}

// Karena oracle sync, bungkus dengan spawn_blocking untuk aplikasi async
async fn dengan_pool_async(
    pool: Arc<OraclePool>,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
    tokio::task::spawn_blocking(move || {
        let conn = pool.get()?;
        let mut stmt = conn.statement("SELECT nama FROM pengguna WHERE ROWNUM <= 10")
            .build()?;
        let rows = stmt.query(&[])?;
        let mut hasil = Vec::new();
        for row_result in rows {
            let row = row_result?;
            let nama: String = row.get(0)?;
            hasil.push(nama);
        }
        Ok(hasil)
    })
    .await?
}

Membuat Tabel — Sintaks Oracle #

-- Sequence untuk auto-increment (Oracle < 12c)
CREATE SEQUENCE seq_pengguna
    START WITH 1
    INCREMENT BY 1
    NOCACHE
    NOCYCLE;

-- Oracle 12c+ mendukung IDENTITY langsung
CREATE TABLE pengguna (
    id          NUMBER(19)      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama        NVARCHAR2(100)  NOT NULL,
    email       NVARCHAR2(255)  NOT NULL,
    password    VARCHAR2(255)   NOT NULL,
    peran       VARCHAR2(20)    DEFAULT 'user' NOT NULL
                                CHECK (peran IN ('user', 'admin', 'moderator')),
    aktif       NUMBER(1)       DEFAULT 1 NOT NULL
                                CHECK (aktif IN (0, 1)),
    dibuat_pada TIMESTAMP       DEFAULT SYSTIMESTAMP NOT NULL,
    diperbarui  TIMESTAMP       DEFAULT SYSTIMESTAMP NOT NULL,
    CONSTRAINT uq_pengguna_email UNIQUE (email)
);

CREATE TABLE artikel (
    id              NUMBER(19)      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    pengguna_id     NUMBER(19)      NOT NULL,
    judul           NVARCHAR2(255)  NOT NULL,
    konten          CLOB            NOT NULL,
    diterbitkan     NUMBER(1)       DEFAULT 0 NOT NULL,
    dibuat_pada     TIMESTAMP       DEFAULT SYSTIMESTAMP NOT NULL,
    CONSTRAINT fk_artikel_pengguna FOREIGN KEY (pengguna_id)
        REFERENCES pengguna(id) ON DELETE CASCADE
);

-- Index
CREATE INDEX idx_artikel_pengguna ON artikel(pengguna_id);
CREATE INDEX idx_artikel_diterbitkan ON artikel(diterbitkan, dibuat_pada DESC);

Struct dan Tipe Data Oracle #

use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Pengguna {
    pub id: i64,                    // NUMBER(19) IDENTITY → i64
    pub nama: String,               // NVARCHAR2 → String
    pub email: String,
    pub password: String,
    pub peran: String,              // VARCHAR2 → String
    pub aktif: bool,                // NUMBER(1) → bool (0/1 ke false/true)
    pub dibuat_pada: NaiveDateTime, // TIMESTAMP → NaiveDateTime
    pub diperbarui: NaiveDateTime,
}

#[derive(Debug, Serialize)]
struct Artikel {
    pub id: i64,
    pub pengguna_id: i64,
    pub judul: String,
    pub konten: String,             // CLOB → String
    pub diterbitkan: bool,
    pub dibuat_pada: NaiveDateTime,
}
Tipe OracleTipe Rust
NUMBER(n)i32, i64, f64 (tergantung presisi)
NUMBER(p,s)f64 atau rust_decimal::Decimal
VARCHAR2(n)String
NVARCHAR2(n)String
CHAR(n)String
CLOBString
BLOBVec<u8>
DATEchrono::NaiveDateTime
TIMESTAMPchrono::NaiveDateTime
TIMESTAMP WITH TIME ZONEchrono::DateTime<Utc>
NUMBER(1) sebagai booli32 (manual konversi ke bool)

Query dan Fetch #

use oracle::{Connection, Error, Row};

fn ambil_semua_pengguna(conn: &Connection) -> Result<Vec<Pengguna>, Error> {
    let mut stmt = conn.statement(
        "SELECT id, nama, email, password, peran, aktif, dibuat_pada, diperbarui
         FROM pengguna
         ORDER BY dibuat_pada DESC"
    ).build()?;

    let rows = stmt.query(&[])?;
    let mut pengguna_list = Vec::new();

    for row_result in rows {
        let row = row_result?;
        let aktif_num: i32 = row.get("AKTIF")?;  // Oracle mengembalikan NUMBER, bukan bool
        pengguna_list.push(Pengguna {
            id: row.get("ID")?,
            nama: row.get("NAMA")?,
            email: row.get("EMAIL")?,
            password: row.get("PASSWORD")?,
            peran: row.get("PERAN")?,
            aktif: aktif_num != 0,  // konversi NUMBER(1) → bool
            dibuat_pada: row.get("DIBUAT_PADA")?,
            diperbarui: row.get("DIPERBARUI")?,
        });
    }

    Ok(pengguna_list)
}

fn cari_pengguna_by_id(conn: &Connection, id: i64) -> Result<Option<Pengguna>, Error> {
    let mut stmt = conn.statement(
        "SELECT id, nama, email, password, peran, aktif, dibuat_pada, diperbarui
         FROM pengguna
         WHERE id = :id"  // Oracle menggunakan :nama untuk named parameters
    ).build()?;

    let rows = stmt.query_named(&[("id", &id)])?;

    for row_result in rows {
        let row = row_result?;
        let aktif_num: i32 = row.get("AKTIF")?;
        return Ok(Some(Pengguna {
            id: row.get("ID")?,
            nama: row.get("NAMA")?,
            email: row.get("EMAIL")?,
            password: row.get("PASSWORD")?,
            peran: row.get("PERAN")?,
            aktif: aktif_num != 0,
            dibuat_pada: row.get("DIBUAT_PADA")?,
            diperbarui: row.get("DIPERBARUI")?,
        }));
    }

    Ok(None)
}

INSERT dengan RETURNING INTO #

Oracle menggunakan RETURNING INTO untuk mendapat nilai setelah INSERT — mirip dengan OUTPUT INSERTED di MSSQL:

fn buat_pengguna(
    conn: &Connection,
    nama: &str,
    email: &str,
    password_hash: &str,
) -> Result<i64, Error> {
    // RETURNING INTO untuk mendapat ID yang baru dibuat
    let mut stmt = conn.statement(
        "INSERT INTO pengguna (nama, email, password)
         VALUES (:nama, :email, :password)
         RETURNING id INTO :id_baru"
    ).build()?;

    stmt.execute_named(&[
        ("nama", &nama),
        ("email", &email),
        ("password", &password_hash),
        ("id_baru", &None::<i64>),  // output parameter
    ])?;

    // Ambil nilai output parameter
    let id_baru: i64 = stmt.returned_value(0, 0)?;
    conn.commit()?;

    Ok(id_baru)
}

// Alternatif dengan sequence (untuk Oracle < 12c tanpa IDENTITY)
fn buat_pengguna_sequence(
    conn: &Connection,
    nama: &str,
    email: &str,
    password_hash: &str,
) -> Result<i64, Error> {
    // Ambil nilai sequence berikutnya
    let mut stmt = conn.statement("SELECT seq_pengguna.NEXTVAL FROM DUAL").build()?;
    let rows = stmt.query(&[])?;
    let mut id_baru: i64 = 0;
    for row in rows {
        id_baru = row?.get(0)?;
    }

    // Insert dengan ID yang sudah diketahui
    conn.execute(
        "INSERT INTO pengguna (id, nama, email, password) VALUES (:1, :2, :3, :4)",
        &[&id_baru, &nama, &email, &password_hash],
    )?;
    conn.commit()?;

    Ok(id_baru)
}

UPDATE dan DELETE #

fn perbarui_pengguna(
    conn: &Connection,
    id: i64,
    nama: &str,
    email: &str,
) -> Result<bool, Error> {
    // Oracle tidak ada SYSDATETIME, gunakan SYSTIMESTAMP
    let baris_terpengaruh = conn.execute_named(
        "UPDATE pengguna
         SET nama = :nama, email = :email, diperbarui = SYSTIMESTAMP
         WHERE id = :id",
        &[("nama", &nama), ("email", &email), ("id", &id)],
    )?;

    conn.commit()?;
    Ok(baris_terpengaruh > 0)
}

fn hapus_pengguna(conn: &Connection, id: i64) -> Result<bool, Error> {
    let baris_terpengaruh = conn.execute(
        "DELETE FROM pengguna WHERE id = :1",
        &[&id],
    )?;

    conn.commit()?;
    Ok(baris_terpengaruh > 0)
}

Transaksi #

Penting: Oracle tidak auto-commit. Setiap koneksi baru dimulai dalam mode transaksional dan membutuhkan COMMIT atau ROLLBACK eksplisit:

fn transfer_data(
    conn: &Connection,
    dari_id: i64,
    ke_id: i64,
    jumlah: f64,
) -> Result<(), Error> {
    // TIDAK perlu BEGIN — Oracle selalu dalam transaksi

    // Lock baris dengan SELECT FOR UPDATE
    let mut stmt = conn.statement(
        "SELECT saldo FROM akun WHERE id = :id FOR UPDATE"
    ).build()?;

    let rows = stmt.query_named(&[("id", &dari_id)])?;
    let mut saldo: f64 = 0.0;
    for row in rows {
        saldo = row?.get(0)?;
    }

    if saldo < jumlah {
        conn.rollback()?;
        return Err(Error::OciError(oracle::OciError::new(
            20001,
            "Saldo tidak mencukupi",
        )));
    }

    conn.execute(
        "UPDATE akun SET saldo = saldo - :1 WHERE id = :2",
        &[&jumlah, &dari_id],
    )?;

    conn.execute(
        "UPDATE akun SET saldo = saldo + :1 WHERE id = :2",
        &[&jumlah, &ke_id],
    )?;

    // COMMIT eksplisit
    conn.commit()?;
    println!("Transfer berhasil");
    Ok(())
}

Stored Procedure dan PL/SQL #

Oracle sangat bergantung pada PL/SQL. Berikut cara memanggil stored procedure dan anonymous block:

-- Stored procedure Oracle
CREATE OR REPLACE PROCEDURE sp_buat_pengguna(
    p_nama      IN  pengguna.nama%TYPE,
    p_email     IN  pengguna.email%TYPE,
    p_password  IN  pengguna.password%TYPE,
    p_id_baru   OUT pengguna.id%TYPE
) AS
BEGIN
    INSERT INTO pengguna (nama, email, password)
    VALUES (p_nama, p_email, p_password)
    RETURNING id INTO p_id_baru;
    COMMIT;
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        RAISE_APPLICATION_ERROR(-20001, 'Email sudah terdaftar');
END;
/
fn panggil_sp_buat_pengguna(
    conn: &Connection,
    nama: &str,
    email: &str,
    password: &str,
) -> Result<i64, Error> {
    // Panggil stored procedure dengan parameter OUT
    let mut stmt = conn.statement(
        "BEGIN sp_buat_pengguna(:nama, :email, :password, :id_baru); END;"
    ).build()?;

    stmt.execute_named(&[
        ("nama", &nama),
        ("email", &email),
        ("password", &password),
        ("id_baru", &None::<i64>),
    ])?;

    let id_baru: i64 = stmt.returned_value(0, 0)?;
    Ok(id_baru)
}

// Anonymous PL/SQL block
fn jalankan_plsql(conn: &Connection, id: i64) -> Result<(), Error> {
    conn.execute(
        "BEGIN
            UPDATE pengguna SET aktif = 0 WHERE id = :1;
            -- Log ke tabel audit
            INSERT INTO audit_log (aksi, target_id, waktu)
            VALUES ('NONAKTIFKAN', :1, SYSTIMESTAMP);
         END;",
        &[&id],
    )?;
    conn.commit()?;
    Ok(())
}

Pagination — Perbedaan dari MySQL dan MSSQL #

-- MySQL
SELECT * FROM pengguna ORDER BY id LIMIT 10 OFFSET 20;

-- MSSQL
SELECT * FROM pengguna ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

-- Oracle 12c+
SELECT * FROM pengguna ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;  -- sama dengan MSSQL!

-- Oracle 11g dan sebelumnya (tanpa FETCH FIRST)
SELECT * FROM (
    SELECT p.*, ROWNUM rn FROM (
        SELECT * FROM pengguna ORDER BY id
    ) p WHERE ROWNUM <= 30  -- offset + limit
) WHERE rn > 20;  -- hapus baris sebelum offset
fn daftar_pengguna_halaman(
    conn: &Connection,
    halaman: i64,
    per_halaman: i64,
) -> Result<Vec<Pengguna>, Error> {
    let offset = (halaman.saturating_sub(1)) * per_halaman;

    // Oracle 12c+ menggunakan sintaks yang sama dengan MSSQL
    let mut stmt = conn.statement(
        "SELECT id, nama, email, password, peran, aktif, dibuat_pada, diperbarui
         FROM pengguna
         ORDER BY id DESC
         OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY"
    ).build()?;

    let rows = stmt.query_named(&[("offset", &offset), ("limit", &per_halaman)])?;
    let mut hasil = Vec::new();

    for row_result in rows {
        let row = row_result?;
        let aktif_num: i32 = row.get("AKTIF")?;
        hasil.push(Pengguna {
            id: row.get("ID")?,
            nama: row.get("NAMA")?,
            email: row.get("EMAIL")?,
            password: row.get("PASSWORD")?,
            peran: row.get("PERAN")?,
            aktif: aktif_num != 0,
            dibuat_pada: row.get("DIBUAT_PADA")?,
            diperbarui: row.get("DIPERBARUI")?,
        });
    }

    Ok(hasil)
}

Perbedaan Penting dari MySQL dan MSSQL #

MySQL/MSSQL                     Oracle

Auto-commit
  Ya (MySQL default)            Tidak — selalu butuh COMMIT eksplisit
  Ya (MSSQL per statement)      Sama — ROLLBACK jika tidak mau simpan

Parameter binding
  ? (MySQL)                     :nama atau :1, :2, :3 (positional)
  @P1 (MSSQL)

Mendapat ID setelah INSERT
  LAST_INSERT_ID() (MySQL)      RETURNING id INTO :var
  OUTPUT INSERTED.id (MSSQL)    atau SELECT seq.NEXTVAL FROM DUAL sebelumnya

Dual table
  Tidak ada                     SELECT 'nilai' FROM DUAL (wajib untuk literal)

Fungsi waktu
  NOW() / GETDATE()             SYSDATE (tanpa waktu) / SYSTIMESTAMP (dengan waktu)

NULL concatenation
  NULL + 'a' = NULL (MySQL)     NULL || 'a' = 'a' (Oracle: NULL diabaikan di ||)

String kosong vs NULL
  '' ≠ NULL                     '' = NULL di Oracle!

Limit/paging
  LIMIT n (MySQL)               FETCH FIRST n ROWS ONLY (Oracle 12c+)
  FETCH NEXT n (MSSQL)          atau ROWNUM <= n (Oracle 11g)

Boolean
  TINYINT(1) / BIT              Tidak ada tipe boolean — gunakan NUMBER(1)

Identifier case
  Tidak sensitive (MySQL)       Case-insensitive kecuali dalam tanda kutip
  Tidak sensitive (MSSQL)       Uppercase secara internal (nama → NAMA)
Di Oracle, string kosong '' sama dengan NULL — ini berbeda dari semua database lain. Jika kamu menyimpan string kosong, Oracle akan menyimpannya sebagai NULL. Selalu gunakan IS NULL bukan = '' untuk memeriksa nilai kosong di Oracle.

Ringkasan #

  • Oracle Instant Client wajib diinstal — ini library C dari Oracle yang dibutuhkan semua driver Oracle, termasuk crate oracle. Set LD_LIBRARY_PATH (Linux) atau DYLD_LIBRARY_PATH (macOS) ke direktori Instant Client.
  • Crate oracle bersifat synchronous — untuk aplikasi async, gunakan tokio::task::spawn_blocking agar operasi database tidak memblokir event loop tokio.
  • Parameter binding menggunakan :nama atau :1 — Oracle menggunakan named parameter (:nama) atau positional (:1, :2), bukan ? (MySQL) atau @P1 (MSSQL).
  • RETURNING id INTO :var untuk mendapat ID setelah INSERT — Oracle tidak punya LAST_INSERT_ID(). Gunakan RETURNING INTO atau sequence NEXTVAL sebelum INSERT.
  • Tidak ada auto-commit — setiap operasi DML (INSERT/UPDATE/DELETE) harus diikuti COMMIT eksplisit atau akan di-rollback saat koneksi ditutup.
  • String kosong '' = NULL di Oracle — tidak ada nilai string kosong di Oracle. Simpan spasi tunggal ' ' jika butuh nilai non-NULL yang “kosong”, atau redesain schema.
  • Kolom nama di-uppercase secara internal — akses kolom dengan row.get("ID") bukan row.get("id"). Identifier Oracle case-insensitive tapi disimpan sebagai uppercase.
  • NUMBER(1) sebagai boolean — Oracle tidak punya tipe BOOL. Konversi manual: let aktif: i32 = row.get("AKTIF")?; let aktif_bool = aktif != 0;
  • Pagination Oracle 12c+ sama dengan MSSQLFETCH FIRST/NEXT n ROWS ONLY. Untuk Oracle 11g dan lebih lama, gunakan subquery bertingkat dengan ROWNUM.

← Sebelumnya: MSSQL   Berikutnya: PostgreSQL →

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