PostgreSQL #
PostgreSQL adalah database pilihan utama komunitas Rust. Jika MySQL cocok untuk aplikasi web umum dan Oracle untuk enterprise legacy, PostgreSQL memberikan fitur terlengkap dengan lisensi open-source: tipe data yang kaya (UUID, array, JSONB, enum), full-text search bawaan, dan kemampuan seperti LISTEN/NOTIFY untuk real-time event. sqlx mendukung PostgreSQL dengan sangat baik — termasuk tipe-tipe eksklusif PostgreSQL seperti UUID, array TEXT[], dan JSONB. Artikel ini membahas seluruh stack PostgreSQL dari Rust: koneksi, query, tipe data unik, transaksi, dan pola-pola yang tidak ada di MySQL atau MSSQL.
Instalasi #
[dependencies]
sqlx = { version = "0.7", features = [
"runtime-tokio-native-tls",
"postgres", # driver PostgreSQL
"macros", # query! dan query_as! macro
"chrono", # DateTime, NaiveDateTime
"uuid", # UUID type
"json", # serde_json::Value untuk JSONB
] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
Install sqlx-cli untuk migrasi:
cargo install sqlx-cli --no-default-features --features native-tls,postgres
Koneksi dan Connection Pool #
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
async fn buat_pool(database_url: &str) -> Result<PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(20)
.min_connections(2)
.acquire_timeout(std::time::Duration::from_secs(5))
.idle_timeout(std::time::Duration::from_secs(300))
.max_lifetime(std::time::Duration::from_secs(1800))
.connect(database_url)
.await
}
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
// Format: postgres://user:password@host:port/database
let url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:password@localhost/contoh".to_string());
let pool = buat_pool(&url).await?;
// Verifikasi dan cek versi
let versi: String = sqlx::query_scalar("SELECT version()")
.fetch_one(&pool)
.await?;
println!("PostgreSQL: {}", &versi[..40]);
// Jalankan migrasi saat startup
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(())
}
Membuat Tabel #
-- migrations/001_buat_tabel.sql
-- Ekstensi untuk UUID (tersedia di semua PostgreSQL modern)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE pengguna (
id BIGSERIAL PRIMARY KEY,
uuid UUID NOT NULL DEFAULT gen_random_uuid(),
nama TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
peran TEXT NOT NULL DEFAULT 'user'
CHECK (peran IN ('user', 'admin', 'moderator')),
aktif BOOLEAN NOT NULL DEFAULT TRUE,
tag TEXT[] NOT NULL DEFAULT '{}', -- array of text
metadata JSONB, -- JSON yang bisa diquery
dibuat_pada TIMESTAMPTZ NOT NULL DEFAULT NOW(),
diperbarui TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE artikel (
id BIGSERIAL PRIMARY KEY,
pengguna_id BIGINT NOT NULL REFERENCES pengguna(id) ON DELETE CASCADE,
judul TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
konten TEXT NOT NULL,
-- Full-text search vector
tsv TSVECTOR GENERATED ALWAYS AS (
to_tsvector('indonesian', judul || ' ' || konten)
) STORED,
diterbitkan BOOLEAN NOT NULL DEFAULT FALSE,
dibuat_pada TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index untuk performa
CREATE INDEX idx_pengguna_email ON pengguna(email);
CREATE INDEX idx_artikel_pengguna ON artikel(pengguna_id);
CREATE INDEX idx_artikel_diterbitkan ON artikel(diterbitkan, dibuat_pada DESC);
-- GIN index untuk full-text search
CREATE INDEX idx_artikel_tsv ON artikel USING GIN(tsv);
-- GIN index untuk JSONB queries
CREATE INDEX idx_pengguna_metadata ON pengguna USING GIN(metadata);
-- Trigger untuk update otomatis kolom diperbarui
CREATE OR REPLACE FUNCTION update_diperbarui()
RETURNS TRIGGER AS $$
BEGIN
NEW.diperbarui = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_pengguna_diperbarui
BEFORE UPDATE ON pengguna
FOR EACH ROW EXECUTE FUNCTION update_diperbarui();
Struct dengan Tipe Eksklusif PostgreSQL #
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, FromRow, Serialize, Deserialize, Clone)]
struct Pengguna {
pub id: i64, // BIGSERIAL → i64
pub uuid: Uuid, // UUID → uuid::Uuid
pub nama: String, // TEXT → String
pub email: String,
#[serde(skip_serializing)]
pub password: String,
pub peran: String,
pub aktif: bool, // BOOLEAN → bool (native, tidak perlu konversi)
pub tag: Vec<String>, // TEXT[] → Vec<String>
pub metadata: Option<serde_json::Value>, // JSONB → Option<serde_json::Value>
pub dibuat_pada: DateTime<Utc>, // TIMESTAMPTZ → DateTime<Utc>
pub diperbarui: DateTime<Utc>,
}
#[derive(Debug, FromRow, Serialize)]
struct Artikel {
pub id: i64,
pub pengguna_id: i64,
pub judul: String,
pub slug: String,
pub konten: String,
pub diterbitkan: bool,
pub dibuat_pada: DateTime<Utc>,
}
| Tipe PostgreSQL | Tipe Rust |
|---|---|
SMALLINT | i16 |
INTEGER | i32 |
BIGINT / BIGSERIAL | i64 |
REAL | f32 |
DOUBLE PRECISION | f64 |
BOOLEAN | bool |
TEXT / VARCHAR | String |
BYTEA | Vec<u8> |
UUID | uuid::Uuid |
TIMESTAMPTZ | chrono::DateTime<Utc> |
TIMESTAMP | chrono::NaiveDateTime |
DATE | chrono::NaiveDate |
TEXT[] / INT[] | Vec<String> / Vec<i32> |
JSONB / JSON | serde_json::Value |
NUMERIC / DECIMAL | rust_decimal::Decimal |
Query dan Fetch #
PostgreSQL menggunakan $1, $2 sebagai parameter binding — berbeda dari ? (MySQL) dan :nama (Oracle). sqlx menangani ini secara otomatis.
use sqlx::PgPool;
async fn ambil_semua_pengguna(pool: &PgPool) -> Result<Vec<Pengguna>, sqlx::Error> {
sqlx::query_as!(
Pengguna,
r#"SELECT id, uuid, nama, email, password, peran, aktif,
tag, metadata, dibuat_pada, diperbarui
FROM pengguna
ORDER BY dibuat_pada DESC"#
)
.fetch_all(pool)
.await
}
async fn cari_pengguna_by_uuid(
pool: &PgPool,
uuid: Uuid,
) -> Result<Option<Pengguna>, sqlx::Error> {
sqlx::query_as!(
Pengguna,
r#"SELECT id, uuid, nama, email, password, peran, aktif,
tag, metadata, dibuat_pada, diperbarui
FROM pengguna
WHERE uuid = $1"#,
uuid // $1 secara otomatis
)
.fetch_optional(pool)
.await
}
// Query dengan array parameter — filter berdasarkan peran yang ada di list
async fn pengguna_dengan_peran(
pool: &PgPool,
peran_list: &[String],
) -> Result<Vec<Pengguna>, sqlx::Error> {
sqlx::query_as!(
Pengguna,
r#"SELECT id, uuid, nama, email, password, peran, aktif,
tag, metadata, dibuat_pada, diperbarui
FROM pengguna
WHERE peran = ANY($1)"#,
peran_list as &[String] // array sebagai parameter
)
.fetch_all(pool)
.await
}
INSERT dengan RETURNING
#
PostgreSQL RETURNING jauh lebih fleksibel dari MySQL — bisa mengembalikan seluruh baris, bukan hanya ID:
async fn buat_pengguna(
pool: &PgPool,
nama: &str,
email: &str,
password_hash: &str,
) -> Result<Pengguna, sqlx::Error> {
// RETURNING * — kembalikan seluruh baris yang baru diinsert
sqlx::query_as!(
Pengguna,
r#"INSERT INTO pengguna (nama, email, password)
VALUES ($1, $2, $3)
RETURNING id, uuid, nama, email, password, peran, aktif,
tag, metadata, dibuat_pada, diperbarui"#,
nama,
email,
password_hash
)
.fetch_one(pool)
.await
}
// INSERT banyak baris sekaligus dengan UNNEST
async fn buat_banyak_pengguna(
pool: &PgPool,
nama_list: &[String],
email_list: &[String],
) -> Result<Vec<i64>, sqlx::Error> {
// UNNEST mengubah array menjadi baris
let ids: Vec<i64> = sqlx::query_scalar!(
r#"INSERT INTO pengguna (nama, email, password)
SELECT nama, email, 'temp_password'
FROM UNNEST($1::TEXT[], $2::TEXT[]) AS t(nama, email)
RETURNING id"#,
nama_list as &[String],
email_list as &[String]
)
.fetch_all(pool)
.await?;
Ok(ids)
}
Update, Upsert, dan DELETE #
async fn perbarui_pengguna(
pool: &PgPool,
id: i64,
nama: &str,
tag_baru: &[String],
) -> Result<Option<Pengguna>, sqlx::Error> {
// RETURNING setelah UPDATE — tidak perlu SELECT terpisah
sqlx::query_as!(
Pengguna,
r#"UPDATE pengguna
SET nama = $1, tag = $2
WHERE id = $3
RETURNING id, uuid, nama, email, password, peran, aktif,
tag, metadata, dibuat_pada, diperbarui"#,
nama,
tag_baru as &[String],
id
)
.fetch_optional(pool)
.await
}
// INSERT OR UPDATE (UPSERT) dengan ON CONFLICT
async fn upsert_pengguna(
pool: &PgPool,
nama: &str,
email: &str,
password_hash: &str,
) -> Result<Pengguna, sqlx::Error> {
sqlx::query_as!(
Pengguna,
r#"INSERT INTO pengguna (nama, email, password)
VALUES ($1, $2, $3)
ON CONFLICT (email) DO UPDATE
SET nama = EXCLUDED.nama,
diperbarui = NOW()
RETURNING id, uuid, nama, email, password, peran, aktif,
tag, metadata, dibuat_pada, diperbarui"#,
nama, email, password_hash
)
.fetch_one(pool)
.await
}
Transaksi dengan SAVEPOINT #
PostgreSQL mendukung SAVEPOINT — titik simpan dalam transaksi yang bisa di-rollback sebagian:
use sqlx::{PgPool, Postgres, Transaction};
async fn proses_batch(
pool: &PgPool,
items: &[(&str, &str)],
) -> Result<Vec<Result<i64, String>>, sqlx::Error> {
let mut tx = pool.begin().await?;
let mut hasil = Vec::new();
for (nama, email) in items {
// Buat savepoint sebelum setiap item
sqlx::query("SAVEPOINT sp_item")
.execute(&mut *tx)
.await?;
match sqlx::query_scalar!(
"INSERT INTO pengguna (nama, email, password)
VALUES ($1, $2, 'temp')
RETURNING id",
nama, email
)
.fetch_one(&mut *tx)
.await
{
Ok(id) => {
// Berhasil — lepas savepoint
sqlx::query("RELEASE SAVEPOINT sp_item")
.execute(&mut *tx)
.await?;
hasil.push(Ok(id));
}
Err(e) => {
// Gagal — rollback ke savepoint (bukan rollback seluruh transaksi)
sqlx::query("ROLLBACK TO SAVEPOINT sp_item")
.execute(&mut *tx)
.await?;
hasil.push(Err(e.to_string()));
}
}
}
tx.commit().await?;
Ok(hasil)
}
Tipe Data Eksklusif PostgreSQL #
Array #
async fn contoh_array(pool: &PgPool) -> Result<(), sqlx::Error> {
// Insert dengan array
sqlx::query!(
"UPDATE pengguna SET tag = $1 WHERE id = $2",
&["rust", "backend", "api"] as &[&str],
1i64
)
.execute(pool)
.await?;
// Query dengan operator array @> (mengandung)
let pengguna_rust: Vec<String> = sqlx::query_scalar!(
"SELECT nama FROM pengguna WHERE tag @> ARRAY['rust']"
)
.fetch_all(pool)
.await?;
println!("Pengguna dengan tag 'rust': {:?}", pengguna_rust);
// Query dengan ANY
let aktif_tertentu: Vec<i64> = sqlx::query_scalar!(
"SELECT id FROM pengguna WHERE $1 = ANY(tag)",
"admin"
)
.fetch_all(pool)
.await?;
Ok(())
}
JSONB #
PostgreSQL JSONB bisa diquery langsung dari SQL:
async fn contoh_jsonb(pool: &PgPool) -> Result<(), sqlx::Error> {
// Insert metadata JSONB
let metadata = serde_json::json!({
"preferensi": {"tema": "gelap", "bahasa": "id"},
"profil": {"kota": "Jakarta", "pekerjaan": "Developer"},
"login_terakhir": "2024-08-24T10:30:00Z"
});
sqlx::query!(
"UPDATE pengguna SET metadata = $1 WHERE id = $2",
metadata,
1i64
)
.execute(pool)
.await?;
// Query field spesifik dalam JSONB dengan operator ->
let tema: Option<serde_json::Value> = sqlx::query_scalar!(
r#"SELECT metadata->'preferensi'->>'tema' FROM pengguna WHERE id = $1"#,
1i64
)
.fetch_optional(pool)
.await?
.flatten();
println!("Tema: {:?}", tema);
// Filter berdasarkan nilai dalam JSONB dengan @>
let dev_jakarta: Vec<String> = sqlx::query_scalar!(
r#"SELECT nama FROM pengguna
WHERE metadata @> '{"profil": {"kota": "Jakarta"}}'::jsonb"#
)
.fetch_all(pool)
.await?;
println!("Developer Jakarta: {:?}", dev_jakarta);
Ok(())
}
Full-Text Search #
async fn cari_artikel_fts(
pool: &PgPool,
query: &str,
) -> Result<Vec<Artikel>, sqlx::Error> {
// websearch_to_tsquery: sintaks yang lebih user-friendly (supports "", -, OR)
sqlx::query_as!(
Artikel,
r#"SELECT id, pengguna_id, judul, slug, konten, diterbitkan, dibuat_pada
FROM artikel
WHERE tsv @@ websearch_to_tsquery('indonesian', $1)
AND diterbitkan = TRUE
ORDER BY ts_rank(tsv, websearch_to_tsquery('indonesian', $1)) DESC
LIMIT 20"#,
query
)
.fetch_all(pool)
.await
}
// Highlight kata yang cocok (snippet)
async fn cari_dengan_highlight(
pool: &PgPool,
query: &str,
) -> Result<Vec<(String, String)>, sqlx::Error> {
let rows = sqlx::query!(
r#"SELECT
judul,
ts_headline(
'indonesian',
konten,
websearch_to_tsquery('indonesian', $1),
'StartSel=<b>, StopSel=</b>, MaxWords=50, MinWords=25'
) AS snippet
FROM artikel
WHERE tsv @@ websearch_to_tsquery('indonesian', $1)
AND diterbitkan = TRUE
ORDER BY ts_rank(tsv, websearch_to_tsquery('indonesian', $1)) DESC
LIMIT 10"#,
query
)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| (r.judul, r.snippet.unwrap_or_default())).collect())
}
LISTEN/NOTIFY — Real-time Event #
PostgreSQL punya mekanisme pub/sub bawaan yang berguna untuk real-time notification antar proses atau koneksi:
use sqlx::postgres::PgListener;
// Listener — menerima notifikasi
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
let url = "postgres://postgres:password@localhost/contoh";
let pool = PgPoolOptions::new().max_connections(5).connect(url).await?;
// Listener untuk menerima notifikasi
let mut listener = PgListener::connect_with(&pool).await?;
listener.listen_all(vec!["pesanan_baru", "pembayaran_berhasil"]).await?;
println!("Mendengarkan notifikasi...");
// Spawn task terpisah untuk mengirim notifikasi
let pool_kirim = pool.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
sqlx::query("SELECT pg_notify('pesanan_baru', $1)")
.bind(r#"{"id": 1001, "total": 150000}"#)
.execute(&pool_kirim)
.await
.unwrap();
println!("Notifikasi dikirim");
});
// Terima notifikasi
while let Ok(notif) = listener.recv().await {
println!(
"Notifikasi di channel '{}': {}",
notif.channel(),
notif.payload()
);
// Parse payload JSON
if let Ok(data) = serde_json::from_str::<serde_json::Value>(notif.payload()) {
println!("Data: {:?}", data);
}
break; // untuk demo — biasanya loop selamanya
}
Ok(())
}
Perbedaan dari MySQL dan MSSQL #
MySQL/MSSQL PostgreSQL
Parameter binding
? (MySQL) $1, $2, $3, ...
@P1 (MSSQL)
RETURNING setelah INSERT
LAST_INSERT_ID() (MySQL) RETURNING id (atau RETURNING *)
OUTPUT INSERTED.id (MSSQL)
Auto-increment
AUTO_INCREMENT (MySQL) BIGSERIAL atau GENERATED ALWAYS AS IDENTITY
IDENTITY(1,1) (MSSQL)
String type
VARCHAR, TEXT (MySQL) TEXT (tidak ada limit, atau VARCHAR(n))
NVARCHAR (MSSQL)
Boolean
TINYINT(1) (MySQL) BOOLEAN (tipe native)
BIT (MSSQL)
UUID
VARCHAR(36) (manual) UUID (tipe native, compact 16 byte)
JSON
JSON (tidak bisa diindex) (MySQL) JSONB (bisa diindex dengan GIN)
(tidak ada JSON type) (MSSQL)
Array
Tidak ada tipe array TEXT[], INT[], BOOLEAN[], dll.
Full-text search
FULLTEXT index (MySQL) TSVECTOR + GIN index (lebih powerful)
Case sensitivity string
Tidak sensitive (MySQL) Sensitive (gunakan ILIKE untuk case-insensitive)
Upsert
INSERT ... ON DUPLICATE KEY UPDATE INSERT ... ON CONFLICT DO UPDATE
Schema
Database = Schema (MySQL) Schema terpisah dalam database
Ringkasan #
sqlx+ PostgreSQL adalah kombinasi terbaik di Rust — async native, compile-time query check, dukungan tipe PostgreSQL yang lengkap termasuk UUID, array, JSONB.- Parameter binding
$1,$2— PostgreSQL menggunakan numbered parameter, berbeda dari?(MySQL).sqlxmenangani ini otomatis dari argumen yang diberikan.RETURNING *setelah INSERT/UPDATE — PostgreSQL bisa mengembalikan seluruh baris yang baru diinsert atau diupdate, sehingga tidak perlu SELECT terpisah.TIMESTAMPTZuntuk timestamp yang timezone-aware — selalu gunakanTIMESTAMPTZ(bukanTIMESTAMP) agar tidak ada ambiguitas zona waktu di lingkungan multi-server.BOOLEANnative — tidak perlu konversi manual dariNUMBER(1)atauBITseperti di Oracle/MSSQL.boolRust langsung dipetakan.JSONBuntuk data semi-terstruktur — bisa diquery dengan operator->,->>,@>, dan diindex dengan GIN. Lebih powerful dari JSON di database lain.TEXT[]untuk array — langsung dipetakan keVec<String>. Gunakan@>untuk “mengandung”,ANY()untuk “salah satunya cocok”.- Full-text search bawaan —
TSVECTOR+GIN index+websearch_to_tsquerymemberikan FTS yang sangat capable tanpa dependensi eksternal.LISTEN/NOTIFY— pub/sub real-time bawaan PostgreSQL, berguna untuk event notification antar service atau invalidasi cache tanpa polling.ON CONFLICT DO UPDATEuntuk upsert — jauh lebih elegan dari MySQLON DUPLICATE KEY UPDATEkarena bisa menggunakanEXCLUDEDuntuk referensi nilai yang ingin diinsert.