13 KiB
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>.pumldan<name>.png - file
.pumldan.pngdiletakkan 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
.pumltanpa.pnghasil render-nya, dan render ulang setiap kali.pumldiubah
Header wajib
Setiap ERD dibuka persis seperti ini. Tanpa judul, tanpa legenda, tanpa blok penulis:
@startuml
hide circle
skinparam linetype ortho
...
@enduml
hide circlemenghapus lingkaran actor di depan nama entity.skinparam linetype orthomembuat garis relasi menyiku/lurus supaya diagram tetap enak dibaca.- sisakan satu baris kosong setelah
@startumldan sebelum@enduml. - jangan menambah
skinparamlain, 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 : tipediikuti 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:
- Tujuan FK:
* <kolom> = <tabel_induk>.id, laluon update cascadedanon delete cascademasing-masing di baris sendiri. - Aturan unique/komposit:
* unique(set, type). - Integritas antar kolom:
* set_type.type must equal type. - Nilai yang diizinkan:
* type availability =lalu daftar nilainya di baris berikutnya, diberi kutip dan dipisah pipe —"llm" | "embedding" | "image generation". - 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:
- Urutan deklarasi
entitydi file tidak berpengaruh. Menulisanaksebeluminduktetap merenderindukdi atas, selama baris relasinyainduk ||--|{ anak. Jangan memperbaiki posisi dengan memindah-mindah blok entity — yang menggeser posisi hanya baris relasi. - 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 kasusmoop_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
packagesendiri — 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
UNIQUEberarti 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:
iddulu, 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 .
-tpnguntuk keluaran PNG,-charset UTF-8supaya 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 blokentitydi 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
.pnghasil render ada di samping.pumldan hasilnya bersih secara visual