Ditulis untuk Hafish, founder LabaBersih

Belajar Tipe Data

Kenapa salah tipe data = uang hilang, data corrupt, atau silent error.


Bab 1: Analogi Sederhana

Bayangkan kamu punya rak di gudang. Setiap rak punya label:

Rak "Nama Produk"  → bisa diisi tulisan apa aja (string)
Rak "Jumlah Stok"  → cuma boleh angka bulat (integer)
Rak "Harga"        → angka desimal, presisi rupiah (decimal)
Rak "Aktif?"       → cuma boleh Ya atau Tidak (boolean)
Rak "Milik Produk" → harus nunjuk ke produk yang BENERAN ADA (FK/UUID)

Kalau kamu taruh tulisan "sepuluh" di rak "Jumlah Stok" — sistem error. Itu tipe data.

Tipe data = aturan tentang ISI APA yang boleh masuk ke setiap kolom database.


Bab 2: 6 Tipe Data yang Kamu Pakai Setiap Hari

1. String — tulisan pendek

Untuk: nama, email, status, SKU, kode.

product.name = "Forbest Ayam 1kg"
product.sku  = "FB-AYM-1KG"
order.status = "dikirim"
Aturan: Maksimal 255 karakter. Kalau lebih panjang → pakai Text.

2. Text — tulisan panjang

Untuk: alamat, catatan, deskripsi.

order.customer_address = "Jl. Raya Kediri No. 123 RT 01/RW 02 Kec. Mojoroto..."
journal.description = "Penjualan TikTok Bestari ID — 31 Maret 2026, 285 order"
Bedanya sama String: Text gak ada batas panjang. Tapi gak boleh di-index (lambat). Jadi JANGAN pakai Text untuk field yang di-search/filter.

3. Integer — angka bulat

Untuk: jumlah barang, quantity.

product.stok = 100
order_item.quantity = 3
product.reserved_stock = 15
Jangan pakai Decimal untuk quantity. Gak ada "2.5 barang". Integer = pasti bulat.

4. Decimal — angka uang (PRESISI)

Untuk: harga, total, fee, HPP — semua yang berhubungan dengan UANG.

product.harga_modal = Decimal.new("45000")    # Rp 45.000
order.total_harga   = Decimal.new("200000")    # Rp 200.000
lot.cost_per_unit   = Decimal.new("47500")     # Rp 47.500
JANGAN PERNAH pakai Float untuk uang. Coba di terminal: 0.1 + 0.2 = 0.30000000000000004

Kalau pakai float: Rp 100.000 + Rp 200.000 bisa jadi Rp 300.000,00000000001. Decimal: PASTI tepat. Rp 100.000 + Rp 200.000 = Rp 300.000. Titik.

5. Boolean — Ya atau Tidak

Untuk: aktif/nonaktif, sudah/belum.

product.is_active = true
entity.is_default = true
store.is_active = false
Hati-hati: Boolean bisa null! Kalau kamu gak set default, nilainya bisa nil (bukan true/bukan false).

if product.is_active do → nil = false. Tapi if !product.is_active do → nil = true!

Selalu set default: is_active, :boolean, default: true

6. UUID / FK — penghubung antar data

Ini yang PALING PENTING dan paling sering bikin masalah.

# UUID = ID unik yang di-generate otomatis
product.id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

# FK = Foreign Key = "kolom ini HARUS nunjuk ke data yang ada di tabel lain"
order.store_id = "b2c3d4e5-f6a7-8901-bcde-f12345678901"
#                ↑ ini HARUS ada di tabel stores

FK itu kayak link yang dijamin valid. Beda sama string:

# String (v1): simpan kode akun sebagai text
store.piutang_account_code = "12-201"
# Database gak tau apakah "12-201" beneran ada
# Kalau salah ketik "12-2O1" (huruf O bukan nol) → SILENT ERROR

# FK (v2): simpan ID akun sebagai reference
store.piutang_account_id = "uuid-of-actual-account"
# Database GUARANTEE: ID ini PASTI ada di tabel accounts
# Kalau coba simpan ID yang gak ada → ERROR LANGSUNG (bukan silent)

Bab 3: Kenapa FK Lebih Aman dari String

Ini kasus nyata dari LabaBersih:

Skenario: Ship order → bikin jurnal penjualan

LANGKAH:
1. Ambil akun piutang dari store
2. Bikin jurnal: Dr. Piutang / Cr. Pendapatan
3. Order status → dikirim

Pakai String (v1) — BAHAYA:

# Store punya piutang_account_code = "12-201"
account = cari_akun_by_code("12-201")

MASALAH 1: Kode "12-201" gak ada (salah input)
  → account = nil
  → Jurnal SKIP (gak dibuat)
  → Order tetap dikirim
  → Revenue HILANG dari laporan keuangan
  → Hafish gak tau sampai akhir bulan

MASALAH 2: Akun "12-201" dihapus
  → Kode masih tersimpan di store sebagai string
  → Ship order → cari "12-201" → gak ketemu → nil → skip
  → Silent error LAGI

MASALAH 3: Kode akun diganti dari "12-201" ke "12-2-101"
  → Store masih simpan "12-201" (string lama)
  → Gak pernah ketemu lagi

Pakai FK (v2) — AMAN:

# Store punya piutang_account_id = UUID
account = ambil_akun_by_id(store.piutang_account_id)

MASALAH 1: ID gak ada?
  → TIDAK MUNGKIN. Database BLOCK insert kalau ID gak valid.
  → Error langsung saat bikin store, bukan saat ship order.

MASALAH 2: Akun dihapus?
  → Database BLOCK delete (on_delete: :restrict)
  → "Gak bisa hapus akun karena masih dipakai oleh store"

MASALAH 3: Kode akun diganti?
  → Gak masalah. FK pakai ID, bukan kode. ID gak pernah berubah.
Kesimpulan: FK = database yang jaga data kamu. String = kamu yang harus jaga sendiri (dan pasti lupa).

Bab 4: Kenapa Salah Tipe Data = Race Condition

Race condition = 2 proses jalan bersamaan, hasilnya kacau.

Contoh: Stok produk

Stok Forbest = 10. Dua orang ship bersamaan.

Tanpa lock (BAHAYA):

Waktu    | Proses A (ship 8)        | Proses B (ship 5)
---------|--------------------------|---------------------------
00:01    | Baca stok = 10           |
00:01    |                          | Baca stok = 10 (SAMA!)
00:02    | 10 - 8 = 2, OK          |
00:02    |                          | 10 - 5 = 5, OK (harusnya GAGAL!)
00:03    | UPDATE stok = 2          |
00:03    |                          | UPDATE stok = 5
---------|--------------------------|---------------------------
Hasil: stok = 5 (terakhir yang UPDATE menang)
Padahal: 8 + 5 = 13 barang terjual dari stok 10!
→ OVERSELL. Barang gak ada tapi order jalan.

Dengan FOR UPDATE lock (AMAN):

Waktu    | Proses A (ship 8)        | Proses B (ship 5)
---------|--------------------------|---------------------------
00:01    | LOCK product row         |
00:01    |                          | Mau LOCK → ANTRI (tunggu A selesai)
00:02    | Baca stok = 10           |
00:03    | 10 - 8 = 2, OK          |
00:04    | UPDATE stok = 2          |
00:05    | COMMIT → RELEASE LOCK    |
00:05    |                          | LOCK acquired!
00:06    |                          | Baca stok = 2 (FRESH, bukan 10!)
00:07    |                          | 2 - 5 = -3 → TOLAK!
---------|--------------------------|---------------------------
Hasil: stok = 2, proses B ditolak. BENAR.
Lock = antrian. Kalau 2 orang mau ubah data yang sama, yang kedua harus TUNGGU yang pertama selesai.

Bab 5: Tabel Keputusan — Kapan Pakai Apa

DataTipeKenapa
Nama produkStringPendek, bisa di-search
AlamatTextBisa sangat panjang
Harga, total, feeDecimalUang harus presisi
Quantity, stokIntegerGak ada 0.5 barang
Aktif/nonaktifBooleanCuma 2 nilai
StatusString + validasiHarus dari list (dikirim/selesai/rts), bukan free text
No. HPString"08123" → integer hilang leading zero
NPWPString"01.234.567.8-901.000" ada titik dan strip
TanggalDate / DateTimeBisa compare, sort, timezone-aware
ID entityUUID (binary_id)Unik, gak bisa ditebak
Referensi ke entity lainFK (UUID)Database jamin data ada
Referensi ke akun (v1)String "12-201"SALAH — silent error
Referensi ke akun (v2)FK (UUID)BENAR — database enforce
Fee mappingJSONB (array of maps)Flexible, jumlah fee beda per toko

Bab 6: 3 Aturan Emas

1. Kalau data A nunjuk ke data B → pakai FK, bukan string.
Database yang jaga, bukan kode kamu.
2. Kalau data bisa diubah bersamaan → pakai lock.
Stok, reserved_stock, saldo — semua yang bisa di-update concurrent.
3. Kalau data soal uang → pakai Decimal, JANGAN Float.
Rp 1 salah di 1000 transaksi = Rp 1000 hilang per hari.

Bab 7: Cara Bertanya yang Efektif

Kalau kamu ketemu masalah data di LabaBersih, tanya seperti ini:

TEMPLATE:

"Di [halaman/fitur apa], saya [melakukan apa],
tapi hasilnya [apa yang salah].

Kode/field yang terlibat: [nama field/tabel].
Error message (kalau ada): [copy paste].

Saya pikir masalahnya di [tebakan kamu]."

Contoh bagus:

"Di halaman pesanan, saya ship order, tapi jurnal gak muncul.
Field: store.piutang_account_code = '12-201'.
Error: gak ada error, tapi jurnal kosong.
Saya pikir kode akun '12-201' gak ketemu di database."

Ini lebih baik dari: "Ship order error, kenapa?"