Memodelkan bagan organisasi di PostgreSQL: adjacency list vs closure
Memodelkan bagan organisasi di PostgreSQL dengan membandingkan adjacency list dan closure table, disertai contoh jelas untuk filter, pelaporan, dan pengecekan izin.

Apa yang perlu didukung oleh bagan organisasi
Bagan organisasi adalah peta siapa melapor kepada siapa, dan bagaimana tim digabungkan ke dalam department. Saat Anda memodelkan bagan organisasi di PostgreSQL, Anda tidak sekadar menyimpan manager_id pada setiap orang. Anda mendukung pekerjaan nyata: penelusuran org, pelaporan, dan aturan akses.
Sebagian besar pengguna mengharapkan tiga hal terasa instan: menjelajahi org, menemukan orang, dan memfilter hasil ke "wilayah saya". Mereka juga mengharapkan pembaruan aman. Saat seorang manager berubah, bagan harus diperbarui di semua tempat tanpa merusak laporan atau izin.
Dalam praktiknya, model yang baik perlu menjawab beberapa pertanyaan berulang:
- Apa rantai komando orang ini (sampai ke puncak)?
- Siapa yang berada di bawah manager ini (laporan langsung dan seluruh subtree)?
- Bagaimana orang dikelompokkan ke tim dan department untuk dashboard?
- Bagaimana reorganisasi terjadi tanpa gangguan?
- Siapa yang bisa melihat apa, berdasarkan struktur org?
Ini jadi lebih sulit daripada pohon sederhana karena organisasi sering berubah. Tim pindah antar department, manager menukar kelompok, dan beberapa tampilan tidak murni "orang melapor ke orang". Misalnya: seorang karyawan tergabung ke tim, dan tim tergabung ke department. Izin menambah lapisan lain: bentuk org menjadi bagian dari model keamanan Anda, bukan sekadar diagram.
Beberapa istilah membantu menjaga desain tetap jelas:
- Node adalah satu item dalam hirarki (orang, tim, atau department).
- Parent adalah node tepat di atasnya (seorang manager, atau department yang memegang tim).
- Ancestor adalah node di atas pada jarak berapa pun (manager dari manager Anda).
- Descendant adalah node di bawah pada jarak berapa pun (semua yang ada di bawah Anda).
Contoh: jika Sales pindah di bawah VP baru, dua hal harus langsung tetap benar. Dashboard masih memfilter "seluruh Sales", dan izin VP baru otomatis mencakup Sales.
Keputusan sebelum memilih desain tabel
Sebelum menetapkan skema, jelaskan apa yang harus dijawab aplikasi Anda setiap hari. "Siapa melapor kepada siapa?" hanyalah awal. Banyak bagan organisasi juga perlu menunjukkan siapa memimpin department, siapa yang menyetujui cuti untuk tim, dan siapa yang dapat melihat laporan.
Tuliskan pertanyaan tepat yang akan diajukan layar dan pengecekan izin Anda. Jika Anda tidak bisa menamai pertanyaannya, Anda akan berakhir dengan skema yang terlihat benar tetapi sulit di-query.
Keputusan yang membentuk semuanya:
- Query mana yang harus cepat: manager langsung, rantai sampai CEO, seluruh subtree di bawah pemimpin, atau "semua orang di department ini"?
- Apakah ini pohon ketat (satu manager) atau organisasi matriks (lebih dari satu manager atau lead)?
- Apakah department menjadi node dalam hirarki yang sama dengan orang, atau atribut terpisah (seperti
department_idpada setiap orang)? - Bisa kah seseorang tergabung ke beberapa tim (shared services, squad)?
- Bagaimana aliran izin: turun pohon, naik pohon, atau keduanya?
Pilihan-pilihan itu menentukan seperti apa data yang "benar". Jika Alex memimpin Support dan Onboarding, satu manager_id atau aturan "satu lead per tim" mungkin tidak bekerja. Anda mungkin membutuhkan tabel join (leader ke team) atau kebijakan jelas seperti "satu tim utama, plus tim garis putus-putus".
Department adalah percabangan lain. Jika department adalah node, Anda bisa mengekspresikan "Department A berisi Team B berisi Person C". Jika department terpisah, Anda akan memfilter dengan department_id = X, yang lebih sederhana tapi bisa gagal ketika tim melintasi department.
Akhirnya, definisikan izin dengan bahasa sederhana. "Seorang manager bisa melihat gaji semua orang di bawahnya, tetapi bukan rekan sejawat" adalah aturan turun-pohon. "Siapa pun bisa melihat rantai manajemennya sendiri" adalah aturan naik-pohon. Putuskan ini lebih awal karena mengubah model hirarki mana yang terasa alami dan mana yang akan menimbulkan query mahal nanti.
Adjacency list: skema sederhana untuk manager dan tim
Jika Anda menginginkan sedikit bagian bergerak, adjacency list adalah titik awal klasik. Setiap orang menyimpan pointer ke manager langsungnya, dan pohon dibuat dengan mengikuti pointer itu.
Setup minimal terlihat seperti ini:
create table departments (
id bigserial primary key,
name text not null unique
);
create table teams (
id bigserial primary key,
department_id bigint not null references departments(id),
name text not null,
unique (department_id, name)
);
create table employees (
id bigserial primary key,
full_name text not null,
team_id bigint references teams(id),
manager_id bigint references employees(id)
);
Anda juga bisa melewatkan tabel terpisah dan menyimpan department_name dan team_name sebagai kolom pada employees. Itu lebih cepat memulai, tetapi lebih sulit dijaga kebersihannya (typo, ganti nama tim, dan pelaporan tidak konsisten). Tabel terpisah memudahkan filter dan aturan izin untuk diekspresikan secara konsisten.
Tambahkan guardrail lebih awal. Data hirarki yang buruk menyakitkan untuk diperbaiki kemudian. Paling tidak, cegah self-management (manager_id <> id). Juga putuskan apakah manager boleh berada di luar tim atau department yang sama, dan apakah Anda perlu soft delete atau perubahan historis (untuk audit garis pelaporan).
Dengan adjacency list, kebanyakan perubahan adalah penulisan sederhana: mengganti manager memperbarui employees.manager_id, dan memindahkan tim memperbarui employees.team_id (sering bersamaan dengan manager). Namun tangkapannya adalah satu penulisan kecil bisa memiliki efek hilir yang besar. Rollup laporan berubah, dan aturan "manager bisa melihat semua laporan" sekarang harus mengikuti rantai baru.
Kesederhanaan ini adalah kekuatan terbesar adjacency list. Kelemahannya muncul ketika Anda sering memfilter "semua orang di bawah manager ini", karena biasanya Anda mengandalkan query rekursif untuk menelusuri pohon setiap kali.
Adjacency list: query umum untuk filter dan pelaporan
Dengan adjacency list, banyak pertanyaan berguna tentang bagan organisasi menjadi query rekursif. Jika Anda memodelkan bagan organisasi di PostgreSQL dengan cara ini, pola-pola ini akan sering Anda jalankan.
Laporan langsung (satu level)
Kasus paling sederhana adalah tim segera seorang manager:
SELECT id, full_name, title
FROM employees
WHERE manager_id = $1
ORDER BY full_name;
Ini cepat dan mudah dibaca, tapi hanya turun satu level.
Rantai komando (ke atas)
Untuk menunjukkan ke siapa seseorang melapor (manager, manager-nya manager, dan seterusnya), gunakan recursive CTE:
WITH RECURSIVE chain AS (
SELECT id, full_name, manager_id, 0 AS depth
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.full_name, e.manager_id, c.depth + 1
FROM employees e
JOIN chain c ON e.id = c.manager_id
)
SELECT *
FROM chain
ORDER BY depth;
Ini mendukung persetujuan, jalur eskalasi, dan breadcrumb manager.
Seluruh subtree (ke bawah)
Untuk mendapatkan semua orang di bawah seorang pemimpin (semua level), balikkan rekursi:
WITH RECURSIVE subtree AS (
SELECT id, full_name, manager_id, department_id, 0 AS depth
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.full_name, e.manager_id, e.department_id, s.depth + 1
FROM employees e
JOIN subtree s ON e.manager_id = s.id
)
SELECT *
FROM subtree
ORDER BY depth, full_name;
Laporan umum adalah "semua orang di department X di bawah pemimpin Y":
WITH RECURSIVE subtree AS (
SELECT id, department_id
FROM employees
WHERE id = $1
UNION ALL
SELECT e.id, e.department_id
FROM employees e
JOIN subtree s ON e.manager_id = s.id
)
SELECT e.*
FROM employees e
JOIN subtree s ON s.id = e.id
WHERE e.department_id = $2;
Query adjacency list bisa berisiko untuk izin karena pengecekan akses sering bergantung pada jalur penuh (apakah viewer adalah ancestor dari orang ini?). Jika satu endpoint lupa rekursi atau menerapkan filter di tempat yang salah, Anda bisa membocorkan baris. Juga waspadai masalah data seperti siklus dan manager yang hilang. Satu record buruk bisa memecah rekursi atau mengembalikan hasil yang mengejutkan, sehingga query izin perlu perlindungan dan constraint yang baik.
Closure table: cara menyimpan seluruh hirarki
Closure table menyimpan setiap hubungan ancestor-descendant, bukan hanya link manager langsung. Alih-alih menelusuri pohon langkah demi langkah, Anda bisa bertanya: "Siapa yang berada di bawah pemimpin ini?" dan mendapat jawaban penuh dengan join sederhana.
Biasanya Anda menyimpan dua tabel: satu untuk node (orang atau tim) dan satu untuk path hirarki.
-- nodes
employees (
id bigserial primary key,
name text not null,
manager_id bigint null references employees(id)
)
-- closure
employee_closure (
ancestor_id bigint not null references employees(id),
descendant_id bigint not null references employees(id),
depth int not null,
primary key (ancestor_id, descendant_id)
)
Closure table menyimpan pasangan seperti (Alice, Bob) yang berarti "Alice adalah ancestor dari Bob". Ia juga menyimpan baris di mana ancestor_id = descendant_id dengan depth = 0. Baris self ini terlihat aneh pada awalnya, tetapi membuat banyak query menjadi lebih bersih.
depth memberi tahu seberapa jauh dua node: depth = 1 adalah manager langsung, depth = 2 adalah manager dari manager, dan seterusnya. Ini berguna ketika laporan langsung harus diperlakukan berbeda dari tidak langsung.
Manfaat utama adalah pembacaan yang dapat diprediksi dan cepat:
- Lookup seluruh subtree cepat (semua orang di bawah director).
- Rantai komando sederhana (semua manager di atas seseorang).
- Anda dapat memisahkan hubungan langsung vs tidak langsung menggunakan
depth.
Biayanya adalah pemeliharaan pada update. Jika Bob mengganti manager dari Alice ke Dana, Anda harus membangun ulang baris closure untuk Bob dan semua orang di bawah Bob. Pendekatan tipikal: hapus path ancestor lama untuk subtree itu, lalu masukkan path baru dengan mengombinasikan ancestor Dana dengan setiap node di subtree Bob dan menghitung ulang depth.
Closure table: query umum untuk filter cepat
Closure table menyimpan setiap pasangan ancestor-descendant terlebih dahulu (sering sebagai org_closure(ancestor_id, descendant_id, depth)). Itu membuat filter org cepat karena sebagian besar pertanyaan menjadi satu join.
Untuk daftar semua orang di bawah seorang manager, join sekali dan filter berdasarkan depth:
-- Descendants (everyone in the subtree)
SELECT e.*
FROM employees e
JOIN org_closure c
ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
AND c.depth > 0;
-- Direct reports only
SELECT e.*
FROM employees e
JOIN org_closure c
ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
AND c.depth = 1;
Untuk rantai komando (semua ancestor dari satu karyawan), balikkan join:
SELECT m.*
FROM employees m
JOIN org_closure c
ON c.ancestor_id = m.id
WHERE c.descendant_id = :employee_id
AND c.depth > 0
ORDER BY c.depth;
Filter menjadi dapat diprediksi. Contoh: "semua orang di bawah pemimpin X, tetapi hanya di department Y":
SELECT e.*
FROM employees e
JOIN org_closure c ON c.descendant_id = e.id
WHERE c.ancestor_id = :leader_id
AND e.department_id = :department_id;
Karena hirarki sudah dipra-komputasi, hitungan juga mudah (tanpa rekursi). Ini membantu dashboard dan total yang digariskan oleh izin, dan cocok dengan pagination serta pencarian karena Anda bisa menerapkan ORDER BY, LIMIT/OFFSET, dan filter langsung pada set descendant.
Bagaimana setiap model memengaruhi izin dan pengecekan akses
Aturan org yang umum adalah sederhana: seorang manager dapat melihat (dan kadang mengedit) semua yang ada di bawahnya. Skema yang Anda pilih mengubah seberapa sering Anda harus membayar biaya untuk mencari tahu "siapa di bawah siapa".
Dengan adjacency list, pengecekan izin biasanya membutuhkan rekursi. Jika pengguna membuka halaman yang menampilkan 200 karyawan, Anda biasanya membangun set descendant dengan recursive CTE dan memfilter baris target terhadapnya.
Dengan closure table, aturan yang sama seringkali bisa dicek dengan tes keberadaan sederhana: "Apakah pengguna saat ini adalah ancestor dari karyawan ini?" Jika ya, izinkan.
-- Closure table permission check (conceptual)
SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
AND c.descendant_id = :employee_id
LIMIT 1;
Kesederhanaan ini penting ketika Anda mengenalkan row-level security (RLS), di mana setiap query otomatis menyertakan aturan seperti "kembalikan hanya baris yang boleh dilihat viewer". Dengan adjacency list, kebijakan seringkali memasukkan rekursi dan bisa lebih sulit di-tune. Dengan closure table, kebijakan sering berupa EXISTS (...) yang lebih langsung.
Kasus tepi adalah tempat logika izin paling sering rusak:
- Dotted-line reporting: satu orang pada dasarnya punya dua manager.
- Asisten dan delegasi: akses tidak berdasarkan hirarki, jadi simpan grant eksplisit (sering dengan expiry).
- Akses sementara: izin berbatas waktu tidak seharusnya dibakar ke struktur org.
- Proyek lintas-tim: berikan akses berdasarkan keanggotaan proyek, bukan rantai manajemen.
Jika Anda membangun ini di AppMaster, closure table seringkali cocok dengan model data visual dan menjaga pengecekan akses tetap sederhana di web dan aplikasi mobile.
Tradeoff: kecepatan, kompleksitas, dan pemeliharaan
Pilihan terbesar adalah apa yang Anda optimalkan: penulisan sederhana dan skema kecil, atau pembacaan cepat untuk "siapa di bawah manager ini" dan pengecekan izin.
Adjacency list menjaga tabel kecil dan update mudah. Biaya muncul pada pembacaan: seluruh subtree biasanya berarti rekursi. Itu bisa cukup jika organisasi Anda kecil, UI cuma memuat beberapa level, atau filter berbasis hirarki hanya dipakai di beberapa tempat.
Closure table membalik tradeoff. Pembacaan menjadi cepat karena Anda bisa menjawab "semua descendant" dengan join reguler. Penulisan menjadi lebih rumit karena sebuah pemindahan atau reorg dapat memerlukan memasukkan dan menghapus banyak baris relasi.
Dalam pekerjaan nyata, tradeoff biasanya terlihat seperti ini:
- Performa baca: adjacency butuh rekursi; closure mostly join dan tetap cepat saat org tumbuh.
- Kompleksitas tulis: adjacency mengupdate satu
parent_id; closure mengupdate banyak baris pada satu pemindahan. - Ukuran data: adjacency tumbuh sebanding orang/tim; closure tumbuh berdasarkan relasi (dalam kasus terburuk, kurang lebih N kuadrat untuk pohon dalam).
Indexing penting di kedua model, tetapi targetnya berbeda:
- Adjacency list: index pointer parent (
manager_id), plus filter umum seperti flag "active". - Closure table: index
(ancestor_id, descendant_id)dan jugadescendant_idsendiri untuk lookup umum.
Aturan sederhana: jika Anda jarang memfilter menurut hirarki dan pengecekan izin hanya "manager melihat laporan langsung", adjacency list sering cukup. Jika Anda sering menjalankan laporan "semua orang di bawah VP X", memfilter berdasarkan pohon department, atau menegakkan izin hirarkis di banyak layar, closure table sering menutup biaya pemeliharaan ekstra.
Langkah demi langkah: berpindah dari adjacency list ke closure table
Anda tidak perlu memilih antara model pada hari pertama. Jalur aman adalah mempertahankan adjacency list (manager_id atau parent_id) dan menambahkan closure table di sampingnya, lalu memigrasikan pembacaan secara bertahap. Ini mengurangi risiko sambil memvalidasi bagaimana hirarki baru berperilaku dalam query nyata dan pengecekan izin.
Mulailah dengan membuat closure table (sering disebut org_closure) dengan kolom seperti ancestor_id, descendant_id, dan depth. Simpan terpisah dari tabel employees atau teams yang ada sehingga Anda bisa backfill dan memvalidasi tanpa menyentuh fitur saat ini.
Rollout praktis:
- Buat closure table dan index sambil mempertahankan adjacency list sebagai sumber kebenaran.
- Backfill baris closure dari hubungan manager saat ini, termasuk baris self (setiap node adalah ancestor dirinya sendiri pada depth 0).
- Validasi dengan pemeriksaan spot: pilih beberapa manager dan konfirmasi set bawahan sama di kedua model.
- Pindahkan jalur baca dulu: laporan, filter, dan izin hirarkis membaca dari closure table sebelum Anda mengubah penulisan.
- Jaga agar closure diperbarui pada setiap penulisan (re-parent, hire, pindah tim). Setelah stabil, hentikan query berbasis rekursi.
Saat memvalidasi, fokus pada kasus yang biasanya merusak aturan akses: perubahan manager, pemimpin tingkat atas, dan pengguna tanpa manager.
Jika Anda membangun di AppMaster, Anda bisa menjalankan endpoint lama sambil menambahkan yang baru yang membaca dari closure table, lalu beralih ketika hasilnya cocok.
Kesalahan umum yang merusak filter org atau izin
Cara tercepat merusak fitur org adalah membiarkan hirarki menjadi tidak konsisten. Data mungkin tampak baik per baris, tetapi kesalahan kecil bisa menyebabkan filter salah, halaman lambat, atau bocornya izin.
Masalah klasik adalah tak sengaja membuat siklus: A mengelola B, dan kemudian seseorang mengatur B mengelola A (atau loop lebih panjang lewat 3–4 orang). Query rekursif bisa berjalan tanpa batas, mengembalikan baris duplikat, atau timeout. Bahkan dengan closure table, siklus bisa meracuni baris ancestor/descendant.
Masalah umum lain adalah closure drift: Anda mengganti manager seseorang, tetapi hanya memperbarui hubungan langsung dan lupa membangun ulang baris closure untuk subtree. Lalu filter seperti "semua orang di bawah VP ini" mengembalikan campuran struktur lama dan baru. Sulit dideteksi karena halaman profil individu masih terlihat benar.
Bagan organisasi juga menjadi berantakan ketika department dan garis pelaporan dicampur tanpa aturan jelas. Department seringkali adalah pengelompokan administratif, sementara garis pelaporan tentang manager. Jika Anda memperlakukan keduanya sebagai pohon yang sama, Anda bisa berakhir dengan perilaku aneh seperti "pemindahan department" yang tak terduga mengubah akses.
Izin paling sering gagal ketika pengecekan hanya melihat manager langsung. Jika Anda memberi akses ketika viewer is manager of employee, Anda melewatkan rantai penuh. Hasilnya adalah terlalu membatasi (manager skip-level tidak dapat melihat org mereka) atau terlalu membagikan (seseorang mendapat akses karena ditetapkan sebagai manager langsung sementara).
Halaman daftar lambat sering berasal dari menjalankan filter rekursif pada setiap permintaan (setiap inbox, daftar tiket, setiap pencarian karyawan). Jika filter yang sama digunakan di mana-mana, Anda ingin path yang diprakomputasi (closure table) atau set ID karyawan yang di-cache daripada menghitung rekursi setiap kali.
Beberapa pengaman praktis:
- Blok siklus dengan validasi sebelum menyimpan perubahan manager.
- Tentukan arti "department" dan pisahkan dari garis pelaporan.
- Jika menggunakan closure table, rebuild baris descendant pada perubahan manager.
- Tulis aturan izin untuk rantai penuh, bukan hanya manager langsung.
- Prakomputasi scope org yang sering digunakan oleh halaman daftar daripada menghitung rekursi setiap kali.
Jika Anda membangun panel admin di AppMaster, perlakukan "ubah manager" sebagai alur sensitif: validasi, perbarui data hirarki terkait, lalu biarkan perubahan memengaruhi filter dan akses.
Pengecekan cepat sebelum rilis
Sebelum menyatakan bagan organisasi Anda "selesai", pastikan Anda bisa menjelaskan akses dengan kata-kata sederhana. Jika seseorang bertanya, "Siapa yang bisa melihat karyawan X, dan kenapa?", Anda harus bisa menunjuk satu aturan dan satu query (atau view) yang membuktikannya.
Performa adalah pemeriksaan kenyataan berikutnya. Dengan adjacency list, "tunjukkan semua orang di bawah manager ini" menjadi query rekursif yang kecepatannya bergantung pada kedalaman dan indexing. Dengan closure table, baca biasanya cepat, tetapi Anda harus mempercayai jalur tulis untuk menjaga tabel tetap benar setelah setiap perubahan.
Checklist singkat sebelum rilis:
- Pilih satu karyawan dan telusuri visibilitas end-to-end: rantai mana yang memberi akses, dan peran mana yang menolaknya.
- Benchmark query subtree manager menggunakan ukuran yang Anda harapkan (misalnya, 5 level dalam dan 50.000 karyawan).
- Blok penulisan buruk: cegah siklus, self-management, dan orphan node dengan constraint dan pengecekan transaksi.
- Uji keselamatan reorg: pemindahan, penggabungan, perubahan manager, dan rollback saat sesuatu gagal di tengah jalan.
- Tambahkan tes izin yang memastikan akses diijinkan dan ditolak untuk peran realistis (HR, manager, team lead, support).
Skenario praktis untuk divalidasi: seorang agen support hanya dapat melihat karyawan di department yang ditugaskan, sedangkan manager dapat melihat seluruh subtree mereka. Jika Anda bisa memodelkan bagan organisasi di PostgreSQL dan membuktikan kedua aturan itu dengan tes, Anda hampir siap untuk rilis.
Jika Anda membangun ini sebagai alat internal di AppMaster, jadikan pemeriksaan ini sebagai tes otomatis pada endpoint yang mengembalikan daftar org dan profil karyawan, bukan hanya query database.
Contoh skenario dan langkah selanjutnya
Bayangkan sebuah perusahaan dengan tiga department: Sales, Support, dan Engineering. Setiap department punya dua tim, dan setiap tim punya lead. Sales Lead A bisa menyetujui diskon untuk timnya, Support Lead B bisa melihat semua tiket untuk departemennya, dan VP Engineering bisa melihat semua yang ada di bawah Engineering.
Lalu terjadi reorg: satu tim Support pindah di bawah Sales, dan manager baru ditambahkan di antara Sales Director dan dua team lead. Keesokan harinya, seseorang meminta akses: "Biarkan Jamie (analis Sales) melihat semua akun pelanggan untuk department Sales, tetapi bukan Engineering."
Jika Anda memodelkan bagan organisasi di PostgreSQL dengan adjacency list, skemanya sederhana, tetapi pekerjaan aplikasi bergeser ke query dan pengecekan izin. Filter seperti "semua orang di bawah Sales" biasanya butuh rekursi. Setelah menambahkan persetujuan (seperti "hanya manager dalam rantai yang bisa menyetujui"), kasus tepi setelah reorg mulai penting.
Dengan closure table, reorg berarti lebih banyak kerja tulis (memperbarui baris ancestor/descendant), tetapi sisi baca menjadi langsung. Filter dan izin seringkali jadi join sederhana: "apakah user ini ancestor dari karyawan itu?" atau "apakah tim ini berada di dalam subtree department ini?".
Ini tampak langsung di layar yang dibangun orang: pemilih orang yang dibatasi ke department, routing persetujuan ke manager terdekat di atas pemohon, view admin untuk dashboard department, dan audit yang menjelaskan kenapa akses ada pada tanggal tertentu.
Langkah selanjutnya:
- Tuliskan aturan izin dengan bahasa sederhana (siapa bisa melihat apa, dan kenapa).
- Pilih model yang sesuai dengan pengecekan paling umum (baca cepat vs tulis lebih sederhana).
- Bangun alat admin internal yang memungkinkan Anda menguji reorg, permintaan akses, dan persetujuan secara end-to-end.
Jika Anda ingin membangun panel admin dan portal yang paham-org dengan cepat, AppMaster (appmaster.io) bisa cocok: ia memungkinkan Anda memodelkan data PostgreSQL, mengimplementasikan logika persetujuan dalam Business Process visual, dan menghasilkan aplikasi web serta native mobile dari backend yang sama.
FAQ
Gunakan adjacency list ketika organisasi Anda kecil, perubahan sering terjadi, dan sebagian besar layar hanya butuh laporan langsung atau beberapa level saja. Gunakan closure table ketika Anda sering membutuhkan “semua orang di bawah pemimpin ini”, filter berdasarkan department, atau izin berbasis hirarki di banyak layar—karena pembacaan menjadi join sederhana dan tetap dapat diprediksi seiring pertumbuhan.
Mulailah dengan employees(manager_id) dan ambil laporan langsung dengan query sederhana WHERE manager_id = ?. Tambahkan query rekursif hanya untuk fitur yang benar-benar membutuhkan seluruh garis keturunan atau subtree, seperti persetujuan, filter “org saya”, atau dashboard skip-level.
Tolak self-management dengan pengecekan seperti manager_id <> id, dan validasi perubahan sehingga Anda tidak menetapkan manager yang sudah berada di subtree karyawan tersebut. Praktiknya, cara paling aman adalah memeriksa ancestry sebelum menyimpan perubahan manager, karena satu siklus bisa merusak rekursi dan logika izin.
Default yang baik adalah memperlakukan department sebagai pengelompokan administratif dan garis pelaporan sebagai pohon manager terpisah. Itu mencegah “pemindahan department” mengubah siapa yang menjadi manager, dan membuat filter seperti “semua orang di Sales” lebih jelas meskipun garis pelaporan tidak cocok dengan batas department.
Biasanya Anda menyimpan manager pelaporan utama pada baris karyawan dan merepresentasikan hubungan dotted-line terpisah, misalnya relasi manager sekunder atau pemetaan “team lead”. Ini menghindari merusak query dasar hirarki sambil tetap memungkinkan aturan khusus seperti akses proyek atau delegasi persetujuan.
Anda harus menghapus path ancestor lama untuk subtree karyawan yang dipindahkan lalu memasukkan path baru dengan mengombinasikan ancestor manager baru dengan setiap node di subtree tersebut, sambil menghitung ulang depth. Lakukan semua ini dalam satu transaksi agar tabel closure tidak pernah berada dalam keadaan setengah-ubah jika terjadi kegagalan.
Untuk adjacency list, index pada employees(manager_id) karena hampir semua query org dimulai dari sana, dan tambahkan index untuk filter umum seperti team_id atau department_id. Untuk closure table, index utama adalah primary key (ancestor_id, descendant_id) dan index terpisah pada descendant_id agar pengecekan “siapa yang bisa melihat baris ini?” cepat.
Polanya umum: izinkan akses ketika ada EXISTS pada tabel closure—viewer adalah ancestor dari employee target. Ini bekerja baik dengan row-level security karena database bisa menerapkan aturan secara konsisten, alih-alih bergantung pada setiap endpoint API mengulang logika rekursif yang sama.
Simpan riwayat secara eksplisit, biasanya dengan tabel terpisah yang merekam perubahan manager beserta tanggal efektif, daripada menimpa manager saat ini dan kehilangan data masa lalu. Ini memungkinkan menjawab “siapa melapor kepada siapa pada tanggal X” tanpa menebak, dan menjaga konsistensi laporan serta audit setelah reorganisasi.
Pertahankan manager_id sebagai sumber kebenaran saat pertama-tama membuat closure table secara paralel dan backfill dari pohon saat ini. Pindahkan jalur baca terlebih dahulu (filter, dashboard, pengecekan izin), lalu buat penulisan memperbarui keduanya, dan berhentikan penggunaan rekursi hanya setelah hasilnya tervalidasi di skenario nyata.


