# Transaction di Postgres

Transaksi di PostgreSQL adalah unit kerja logis yang terdiri dari satu atau lebih pernyataan SQL yang dijalankan sebagai satu kesatuan. Tujuan utamanya adalah untuk menjaga **konsistensi, isolasi, dan integritas data**.

***

### 🔄 Konsep Transaksi di PostgreSQL

#### 1. **ACID**

Transaksi harus memenuhi sifat **ACID**:

* **Atomicity**: Semua pernyataan dalam transaksi berhasil semua atau dibatalkan semua.
* **Consistency**: Data tetap konsisten sebelum dan sesudah transaksi.
* **Isolation**: Transaksi tidak terlihat oleh transaksi lain sampai selesai.
* **Durability**: Setelah commit, data tersimpan permanen.

#### 2. **Perintah Dasar**

```sql
BEGIN;           -- Memulai transaksi
-- Perintah SQL
COMMIT;          -- Menyimpan perubahan
ROLLBACK;        -- Membatalkan semua perubahan
```

***

### 🔒 Apa itu `FOR UPDATE`?

`FOR UPDATE` digunakan untuk **mengunci baris** yang dipilih dalam transaksi agar tidak bisa diubah oleh transaksi lain **sampai transaksi selesai (commit/rollback)**.

#### Tujuannya:

* Mencegah kondisi **race condition** (misalnya dua pengguna mengedit data yang sama).
* Memastikan data yang dibaca tidak berubah sebelum transaksi selesai.

***

### 🎯 Contoh dan Ilustrasi

#### 1. **Setup Tabel**

```sql
CREATE TABLE akun (
    id SERIAL PRIMARY KEY,
    nama VARCHAR(50),
    saldo INTEGER
);

INSERT INTO akun (nama, saldo) VALUES
('Alice', 1000),
('Bob', 1000);
```

***

#### 2. **Simulasi Transaksi tanpa `FOR UPDATE`**

**Terminal 1:**

```sql
BEGIN;
SELECT saldo FROM akun WHERE nama = 'Alice';  -- dapat saldo 1000
-- bayangkan proses lanjut menghitung dan menyimpan
UPDATE akun SET saldo = 900 WHERE nama = 'Alice';
-- COMMIT nanti
```

**Terminal 2 (dijalankan sebelum commit Terminal 1):**

```sql
BEGIN;
SELECT saldo FROM akun WHERE nama = 'Alice'; -- juga dapat saldo 1000!
UPDATE akun SET saldo = 800 WHERE nama = 'Alice';
COMMIT;
```

**Masalah**: Kedua transaksi menganggap saldo awal 1000 dan memperbarui berdasarkan data yang sudah kadaluarsa → terjadi **inconsistency**.

***

#### 3. **Simulasi Transaksi dengan `FOR UPDATE`**

**Terminal 1:**

```sql
BEGIN;
SELECT saldo FROM akun WHERE nama = 'Alice' FOR UPDATE;
-- Alice dikunci barisnya sampai transaksi ini selesai
UPDATE akun SET saldo = 900 WHERE nama = 'Alice';
-- Jangan commit dulu
```

**Terminal 2:**

```sql
BEGIN;
SELECT saldo FROM akun WHERE nama = 'Alice' FOR UPDATE;
-- Akan menunggu sampai transaksi di Terminal 1 selesai (commit/rollback)
```

**Keuntungan**: Data tidak bisa dibaca dan diproses oleh transaksi lain sebelum transaksi pertama selesai → **race condition dihindari**.

***

### 🧪 Praktek Langsung: Transfer Saldo

Misal: Transfer 100 dari Alice ke Bob

```sql
BEGIN;

-- Kunci baris yang ingin dimodifikasi
SELECT * FROM akun WHERE nama IN ('Alice', 'Bob') FOR UPDATE;

-- Dapatkan saldo
SELECT saldo FROM akun WHERE nama = 'Alice';  -- 1000
SELECT saldo FROM akun WHERE nama = 'Bob';    -- 1000

-- Update
UPDATE akun SET saldo = saldo - 100 WHERE nama = 'Alice';
UPDATE akun SET saldo = saldo + 100 WHERE nama = 'Bob';

COMMIT;
```

Jika terjadi error, bisa langsung:

```sql
ROLLBACK;
```

***

### 🧠 Kesimpulan

| Konsep       | Penjelasan                                                         |
| ------------ | ------------------------------------------------------------------ |
| Transaksi    | Sekumpulan perintah SQL yang dieksekusi sebagai satu kesatuan      |
| BEGIN/COMMIT | Memulai dan menyelesaikan transaksi                                |
| ROLLBACK     | Membatalkan transaksi                                              |
| FOR UPDATE   | Mengunci baris yang dipilih untuk mencegah konflik antar transaksi |

***

### ⚠️ Apa Itu Deadlock?

**Deadlock** terjadi ketika **dua transaksi saling menunggu satu sama lain** untuk melepaskan kunci, sehingga tidak ada yang bisa lanjut.

#### 🔁 Ilustrasi Kasus Deadlock

Misalkan kita punya tabel `akun` yang sama:

```sql
CREATE TABLE akun (
    id SERIAL PRIMARY KEY,
    nama VARCHAR(50),
    saldo INTEGER
);
```

***

### 🧪 Simulasi Deadlock dengan `FOR UPDATE`

#### **Transaksi 1 (T1):**

```sql
BEGIN;
SELECT * FROM akun WHERE nama = 'Alice' FOR UPDATE;
-- tunggu beberapa detik
SELECT * FROM akun WHERE nama = 'Bob' FOR UPDATE;
```

#### **Transaksi 2 (T2):**

```sql
BEGIN;
SELECT * FROM akun WHERE nama = 'Bob' FOR UPDATE;
-- tunggu beberapa detik
SELECT * FROM akun WHERE nama = 'Alice' FOR UPDATE;
```

🔁 **Hasil**:

* T1 mengunci Alice → tunggu Bob
* T2 mengunci Bob → tunggu Alice
* Keduanya saling tunggu → **deadlock**
* PostgreSQL akan memilih satu untuk dibatalkan (`ERROR: deadlock detected`)

***

### 🛠️ Kenapa Bisa Deadlock Walau Pakai `FOR UPDATE`?

Karena `FOR UPDATE` **mengunci baris secara eksklusif**. Kalau dua transaksi mengunci **dengan urutan berbeda**, maka bisa saling tunggu. Urutan pengambilan kunci itu penting.

***

### ✅ Solusi: Gunakan `FOR NO KEY UPDATE`

#### ❓ Apa Itu `FOR NO KEY UPDATE`?

* `FOR NO KEY UPDATE` adalah **varian lebih ringan dari `FOR UPDATE`**.
* Mengunci baris **hanya untuk update nilai kolom**, **tanpa mengizinkan update kunci (misalnya kolom primary key)**.
* Ini mengurangi kemungkinan deadlock karena kunci yang digunakan tidak terlalu agresif.

***

### 📊 Perbandingan Lock Mode

| Lock Mode           | Deskripsi                                                                    |
| ------------------- | ---------------------------------------------------------------------------- |
| `FOR UPDATE`        | Mengunci baris untuk update penuh (termasuk kunci)                           |
| `FOR NO KEY UPDATE` | Mengunci baris untuk update non-kunci (lebih ringan)                         |
| `FOR SHARE`         | Hanya baca data, bisa dibaca transaksi lain                                  |
| `FOR KEY SHARE`     | Bisa dibaca dan referensi foreign key, tapi tidak bisa dihapus/dikunci penuh |

***

### 🧪 Simulasi dengan `FOR NO KEY UPDATE`

Jika transaksi hanya ingin update saldo (bukan `id`), maka lebih aman:

#### **T1:**

```sql
BEGIN;
SELECT * FROM akun WHERE nama = 'Alice' FOR NO KEY UPDATE;
-- tunggu
SELECT * FROM akun WHERE nama = 'Bob' FOR NO KEY UPDATE;
```

#### **T2:**

```sql
BEGIN;
SELECT * FROM akun WHERE nama = 'Bob' FOR NO KEY UPDATE;
-- tunggu
SELECT * FROM akun WHERE nama = 'Alice' FOR NO KEY UPDATE;
```

⚠️ **Hasil**:

* Deadlock masih mungkin terjadi **kalau urutan ambil lock-nya berbeda**, **meskipun lebih jarang**.
* Maka **kunci solusi utamanya tetap: urutkan pengambilan kunci secara konsisten!**

***

### ✅ Best Practice Hindari Deadlock

1. **Selalu akses resource dalam urutan yang sama** (misal: urut berdasarkan `id` ASC).
2. Gunakan lock mode serendah mungkin (`FOR NO KEY UPDATE` lebih baik dari `FOR UPDATE`).
3. Gunakan timeout / pengecekan deadlock dan rollback otomatis jika perlu.

***

### 🔚 Kesimpulan

* `FOR UPDATE` kuat tapi agresif → rawan deadlock jika resource diakses tidak konsisten.
* `FOR NO KEY UPDATE` lebih ringan → cocok jika hanya update kolom non-kunci.
* **Urutan pengambilan kunci lebih penting dari jenis lock-nya.**

***

### 🔍 Apa Maksudnya "Pengambilan Kunci Secara Konsisten"?

Artinya: **semua transaksi yang mengakses banyak baris/data harus mengunci data dalam urutan yang sama**, misalnya:

* berdasarkan `id` ASC
* berdasarkan abjad nama
* urutan waktu tertentu

❌ Jika Transaksi A mengunci data dengan urutan A → B\
dan Transaksi B mengunci data dengan urutan B → A,\
⚠️ maka potensi **deadlock** sangat tinggi.

✅ Jika semua transaksi **selalu mengunci A → B**, maka tidak akan ada saling tunggu silang.

***

### 🔁 Contoh Deadlock karena Kunci Tidak Konsisten

Misal tabel `akun`:

```sql
id | nama   | saldo
---+--------+-------
1  | Alice  | 1000
2  | Bob    | 1000
```

#### Transaksi A

```sql
BEGIN;
SELECT * FROM akun WHERE id = 1 FOR UPDATE; -- kunci Alice
SELECT * FROM akun WHERE id = 2 FOR UPDATE; -- kunci Bob
```

#### Transaksi B

```sql
BEGIN;
SELECT * FROM akun WHERE id = 2 FOR UPDATE; -- kunci Bob
SELECT * FROM akun WHERE id = 1 FOR UPDATE; -- tunggu Alice
```

🔁 Keduanya saling tunggu → **Deadlock**!

***

### ✅ Solusi: Ambil Kunci Berdasarkan Urutan Tetap

#### Cara Aman:

```sql
-- Ambil data dengan urutan konsisten (id ASC misalnya)
SELECT * FROM akun WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
```

Jadi, transaksi **A dan B**:

```sql
BEGIN;
SELECT * FROM akun WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- lanjut proses transfer atau update
COMMIT;
```

✅ Hasil: **tidak ada saling tunggu silang**, karena kedua transaksi:

* mengakses `id = 1` lebih dulu
* baru `id = 2` berikutnya

***

### 🧪 Contoh Praktis: Transfer Saldo Aman

```sql
-- Asumsikan kita mau transfer antar 2 akun
-- Ambil id akun pengirim dan penerima, lalu sort ASC

-- Misal id_pengirim = 2, id_penerima = 1

-- Sort dulu:
SELECT LEAST(2, 1) AS id1, GREATEST(2, 1) AS id2;

-- Gunakan urutan itu untuk locking
BEGIN;

-- Ambil dan kunci dua akun berdasarkan urutan id
SELECT * FROM akun WHERE id IN (1, 2) ORDER BY id FOR UPDATE;

-- Update saldo
UPDATE akun SET saldo = saldo - 100 WHERE id = 2;
UPDATE akun SET saldo = saldo + 100 WHERE id = 1;

COMMIT;
```

***

### 🧠 Ringkasan Tips

| Tips Pengambilan Kunci Konsisten             | Penjelasan                                                                |
| -------------------------------------------- | ------------------------------------------------------------------------- |
| 🔁 **Gunakan urutan tetap**                  | Gunakan `ORDER BY` berdasarkan `id`, `nama`, atau kriteria stabil lainnya |
| 🚫 **Jangan kunci berbeda-beda urutan**      | Hindari transaksi A → B dan B → A                                         |
| ⚖️ **Gunakan `LEAST` / `GREATEST`**          | Untuk menentukan siapa dikunci lebih dulu secara deterministik            |
| 🔐 **Gabungkan dalam satu query**            | Semakin sedikit query terpisah, semakin kecil peluang deadlock            |
| 💡 **Gunakan lock ringan (`NO KEY UPDATE`)** | Kalau tidak perlu ubah PK atau referensi, pakai lock lebih ringan         |

***
