hendrik/instruction/erd/erd-plantuml-create.md
2026-10-11 22:36:16 +07:00

13 KiB
Raw Blame History

ERD / PlantUML / Buat

Berikut panduan cara membuat ERD dengan PlantUML.

Acuan format standarnya adalah plan/milestone/moop.puml (sumber) dan plan/milestone/moop.png (hasil render). Ikuti gaya itu persis — header-nya sama, tata letak entity-nya sama, cara menulis constraint dan relasinya juga sama.

Yang perlu disiapkan:

  • prefix modul/domain (misal moop, chat, memories) — dipakai sebagai prefix nama tabel/entity
  • daftar tabel beserta kolomnya
  • relasi antar tabel (siapa mereferensi siapa, wajib atau boleh kosong)
  • direktori tujuan untuk pasangan file .puml + .png (bawaan: plan/milestone/)

Bahasa penulisan: isi diagram tetap bahasa Inggris (nama tabel, tipe, dan teks constraint seperti on update cascade / null priority = disabled) supaya konsisten dengan skema SQL dan moop.puml. Penjelasan di dokumen ini saja yang bahasa Indonesia.

Penamaan file dan lokasinya

  • satu diagram = satu pasangan file: <name>.puml dan <name>.png
  • file .puml dan .png diletakkan berdampingan di direktori yang sama
  • nama diagram mengikuti nama fitur/modul, kebab-case atau huruf kecil biasa (contoh: moop.puml, 2-basic-features.puml)
  • jangan pernah men-commit .puml tanpa .png hasil render-nya, dan render ulang setiap kali .puml diubah

Header wajib

Setiap ERD dibuka persis seperti ini. Tanpa judul, tanpa legenda, tanpa blok penulis:

@startuml

hide circle
skinparam linetype ortho

...

@enduml
  • hide circle menghapus lingkaran actor di depan nama entity.
  • skinparam linetype ortho membuat garis relasi menyiku/lurus supaya diagram tetap enak dibaca.
  • sisakan satu baris kosong setelah @startuml dan sebelum @enduml.
  • jangan menambah skinparam lain, warna, atau blok <style>. File ERD tetap polos.

Kelompokkan tabel dengan package

Cluster entity berdasarkan sub-domain memakai package <nama> { ... } (huruf kecil, sedapatnya satu kata, contoh: option, provider). Semua entity dalam satu ranah dimasukkan ke package yang sama, dan relasi ranah itu juga ditulis di dalam blok yang sama. Antar package dipisah satu baris kosong.

package provider {

    ...entity dan relasi milik domain provider...

}

Blok entity

Nama entity adalah nama tabelnya, apa adanya: berprefix nama modul, snake_case, tunggal atau jamak mengikuti skema aslinya (contoh: moop_model_set, moop_api).

    entity moop_model {
        id : binary(40) <<PK>>
        api : binary(40) <<FK>>
        set : binary(40) <<FK>>
        model : longtext
        priority : int null
        --
        * api = moop_api.id
          on update cascade
          on delete cascade
        * set = moop_model_set.id
          on update cascade
          on delete cascade
        * null priority = disabled
    }

Aturannya:

  • isi entity di-indent 4 spasi (kalau di dalam package: 8 spasi untuk atribut).
  • satu atribut per baris, formatnya nama : tipe diikuti penanda stereotype.
  • -- memisahkan bagian kolom dari bagian constraint/aturan. Selalu tulis pemisahnya, walaupun di bawahnya tidak ada apa-apa.
  • tidak ada baris kosong di dalam blok entity.
  • jangan pakai ekstensi <<primary key>>/{mask} dari PlantUML, jangan pakai warna, jangan pakai alias (entity "foo" as f). Nama di sebelah kiri ya nama tabel aslinya.

Sintaks atribut

Kondisi Sintaks Contoh
primary key id : binary(40) <<PK>> selalu baris pertama, selalu binary(40)
foreign key api : binary(40) <<FK>> kolom FK juga binary(40)
kolom unique type : binary(40) unique <<FK>> unique ditulis tepat setelah tipe
boleh null priority : int null null ditulis setelah tipe
teks name : longtext
angka priority : int

Tipe ditulis sebagai tipe SQL (binary(40), longtext, int), bukan tipe Python. Kunci surrogate dan semua referensi ke baris tabel lain memakai binary(40).

Bagian constraint

Setiap baris di bawah -- diawali * . Aturan yang panjangnya lebih dari satu baris dilanjutkan di baris berikutnya dengan indent lebih dalam, tanpa mengulang tanda *.

Tulis di sini, berurutan seperti ini:

  1. Tujuan FK: * <kolom> = <tabel_induk>.id, lalu on update cascade dan on delete cascade masing-masing di baris sendiri.
  2. Aturan unique/komposit: * unique(set, type).
  3. Integritas antar kolom: * set_type.type must equal type.
  4. Nilai yang diizinkan: * type availability = lalu daftar nilainya di baris berikutnya, diberi kutip dan dipisah pipe — "llm" | "embedding" | "image generation".
  5. Catatan bisnis/semantik dengan kalimat biasa: * null priority = disabled, * empty key = no bearer, * one default set per model type.

Bagian constraint inilah yang membuat diagram bisa di-review tanpa membaca SQL-nya. Kalau ada aturan di skema atau di spesifikasi produk, tempatnya ada di sini.

Relasi

Tulis setiap relasi di baris sendiri, tepat setelah blok entity yang memilikinya (entity anak yang punya kolom FK, atau induknya kalau relasi itu memang dikenalkan dari sana). Tanpa label, tanpa klausa to, tanpa baris kosong antara entity dan relasinya.

    moop_model_set ||--|{ moop_model_set_type
    moop_model_type ||-o{ moop_model_set_type
    moop_model_type ||--|| moop_model_type_default
    moop_model_set_type ||--o| moop_model_type_default
    moop_api ||--o{ moop_key

Kardinalitas selalu induk <--> anak, memakai notasi crow-foot:

Kiri (induk) Arti Kanan (anak) Arti
|| tepat satu || tepat satu
|| tepat satu |{ satu atau lebih
|| tepat satu o{ nol atau lebih
|| tepat satu o| nol atau satu

Simbolnya: || = wajib satu, o| = opsional satu, |{ = satu atau lebih, o{ = nol atau lebih.

Jumlah dash bukan kardinalitas. Yang membawa makna hanya karakter di kedua ujungnya (|, o, {, }). Jumlah - adalah knob tata letak yang mengatur orientasi:

Sintaks Efek (sudah diverifikasi ke jar-nya)
|- (satu dash) horizontal — kedua entity berdampingan, edge-nya tidak menambah rank
|-- (dua dash) vertikal — entity anak ditumpuk di bawah induknya. Ini bawaannya
|---, |---- tetap vertikal, cuma lebih panjang. Pakai saat garis saling menutupi

Diukur pada rantai 3 entity yang identik: satu dash merender 543×95 (a→b→c berjajar), dua dash merender 171×349 (a→b→c menurun). Menambah dash ke tiga atau empat hanya menambah tinggi (275 → 336 px), tidak pernah mengubah orientasi.

Ini disengaja di moop.puml: moop_model_type ||-o{ moop_model_set_type memakai satu dash supaya moop_model_type jatuh ke sebelah kiri tabel junction, sedangkan semua relasi induk→anak memakai dua dash supaya diagram tetap mengalir dari atas ke bawah.

Jadi aturannya:

  • pakai -- sebagai default untuk semua relasi — diagramnya terbaca sebagai hierarki vertikal
  • pindah ke satu - hanya kalau dua tabel memang perlu berdampingan agar render tidak kusut
  • jangan pernah memakai jumlah dash untuk arti "kuat vs lemah" atau "identifying vs non-identifying". Makna itu tidak ada, dan pembaca akan salah tafsir
  • jangan pakai .. (garis putus-putus). Hasilnya garis dashed dan tidak dipakai sama sekali di format standar ini

Menentukan posisi entity (mana yang atas/bawah, mana yang kiri/kanan)

Sisi penulisan entity pada baris relasi adalah posisinya. Entity di sisi kiri akan ditempatkan di atas (relasi vertikal) atau di sebelah kiri (relasi horizontal) dari entity di sisi kanan. Sudah diverifikasi ke jar-nya:

Baris relasi Hasil
induk |--|{ anak induk di atas, anak di bawah
anak }--|| induk anak di atas, induk di bawah (terbalik)
induk |-o{ anak induk di kiri, anak di kanan
anak }o-|| induk anak di kiri, induk di kanan (terbalik)

Dua konsekuensi yang penting:

  1. Urutan deklarasi entity di file tidak berpengaruh. Menulis anak sebelum induk tetap merender induk di atas, selama baris relasinya induk ||--|{ anak. Jangan memperbaiki posisi dengan memindah-mindah blok entity — yang menggeser posisi hanya baris relasi.
  2. Menulis barisnya terbalik akan membalik diagram dan merusak notasi. Pertahankan induk di sisi kiri, supaya crow's foot jatuh di sisi anak. Kalau karena alasan tata letak anak harus muncul di atas induk, pindahkan package/entity-nya dan tetap tulis induk <--> anak, jangan membalik baris relasinya.

Resep praktis supaya hasilnya enak dilihat:

  • gambar hierarki utama dari atas ke bawah dengan --, induk selalu di kiri
  • untuk tabel junction, pisahkan dua induknya: induk utama di atasnya dengan ||--|{, induk lookup di sampingnya dengan ||-o{ (satu dash) supaya induk itu jatuh ke kiri, bukan menumpuk — persis kasus moop_model_type
  • kalau dua entity sibling saling desak, beri satu dash tambahan pada salah satu relasinya (||---|{) untuk membuka ruang vertikal
  • kalau rantai tabelnya terlalu panjang sampai diagramnya terlalu tinggi, pecah per sub-domain ke package sendiri — bukan dengan membalik baris relasi

Cek konsistensi yang wajib dilakukan:

  • kalau kolom FK-nya NOT NULL, sisi anak harus memakai | (||--|{, ||--o|)
  • kalau kolom FK-nya nullable/opsional, sisi anak memakai o (||-o{, ||--o|)
  • kolom FK yang UNIQUE berarti relasi 1-ke-1 (||--|| atau ||--o|), bukan 1-ke-banyak
  • setiap <<FK>> yang dideklarasikan di entity harus punya baris relasi yang cocok di suatu tempat, dan setiap baris relasi harus punya kolom FK yang cocok. Tidak boleh ada relasi yatim, tidak boleh ada FK yang tidak terdokumentasi.

Relasi antar package

Kalau sebuah tabel di satu package mereferensi tabel di package lain, tetap deklarasikan FK-nya di dalam entity anak dan gambar relasinya dari sisi anak, di dalam blok package anaknya. Jangan membuat bagian "relations" terpisah di bawah file.

Urutan penulisan

Aturan di bagian ini hanya untuk keterbacaan sumbernya — tidak mengubah tata letak hasil render. Posisi ditentukan oleh baris relasi (lihat "Menentukan posisi entity").

  • package: dari domain yang paling mandiri ke yang paling bergantung
  • di dalam package: tabel induk dulu, lalu tabel junction/anaknya, lalu tabel yang bergantung pada tabel-tabel itu
  • atribut: id dulu, lalu FK dan kolom data, lalu kolom nullable/opsional paling akhir

Render

Menggunakan jar yang sudah tersedia di plan/milestone/:

cd plan/milestone && java -jar plantuml-mit-1.2026.8.jar -tpng -charset UTF-8 moop.puml -o .
  • -tpng untuk keluaran PNG, -charset UTF-8 supaya nilai enum yang dikutip tidak rusak
  • -o <dir> menulis gambar di samping file sumbernya
  • setelah render, buka .png-nya dan pastikan: diagramnya mengalir atas ke bawah, tidak ada kotak yang tumpang tindih, tidak ada garis bersilangan yang sebenarnya bisa dihindari dengan memindah satu relasi ke satu dash (berdampingan) atau menambah dash untuk membuka ruang vertikal, setiap entity berada di tepat satu package, dan setiap relasi benar-benar tersambung. Ingat: memindah blok entity di sumber tidak menggeser apa pun — yang menggeser hanya baris relasi

Template minimal

@startuml

hide circle
skinparam linetype ortho

package <domain> {

    entity <prefix>_parent {
        id : binary(40) <<PK>>
        name : longtext
        --
    }

    entity <prefix>_type {
        id : binary(40) <<PK>>
        type : longtext
        --
        * type availability =
          "a" | "b" | "c"
    }

    entity <prefix>_parent_type {
        id : binary(40) <<PK>>
        parent : binary(40) <<FK>>
        type : binary(40) <<FK>>
        --
        * parent = <prefix>_parent.id
          on update cascade
          on delete cascade
        * type = <prefix>_type.id
          on update cascade
          on delete cascade
        * unique(parent, type)
    }
    <prefix>_parent ||--|{ <prefix>_parent_type
    <prefix>_type ||-o{ <prefix>_parent_type

    entity <prefix>_child {
        id : binary(40) <<PK>>
        parent : binary(40) <<FK>>
        priority : int null
        --
        * parent = <prefix>_parent.id
          on update cascade
          on delete cascade
        * null priority = disabled
    }
    <prefix>_parent ||--o{ <prefix>_child

}

@enduml

Checklist review

  • @startuml / hide circle / skinparam linetype ortho / @enduml, tidak ada yang lain di header
  • semua entity dikelompokkan dalam package, tidak ada entity mengambang
  • nama entity = nama tabel asli, berprefix modul, snake_case
  • id : binary(40) <<PK>> jadi atribut pertama di setiap entity
  • pemisah -- ada di setiap entity
  • kolom FK ditandai <<FK>> dan diulang di bagian constraint beserta aturan cascade-nya
  • nilai enum/domain diberi kutip dan dipisah pipe
  • setiap relasi di baris sendiri, tepat setelah entity-nya, induk di sisi kiri
  • kardinalitas cocok dengan nullability dan uniqueness FK
  • jumlah dash hanya dipakai sebagai knob tata letak (- = berdampingan, -- = menumpuk), bukan sebagai makna. Tanpa .., tanpa tafsir "kuat vs lemah"
  • induk selalu ditulis di sisi kiri setiap baris relasi, supaya dia yang muncul di atas / di kiri
  • .png hasil render ada di samping .puml dan hasilnya bersih secara visual