[**Database**](http://pojokprogrammer.net/tags/database) [**Tips**](http://pojokprogrammer.net/tags/tips-dan-trik) | Semua programmer yang berkonsentrasi di pengembangan aplikasi perkantoran (*office application*) pasti pernah berhadapan dengan data hirarki (***hierarchical data***) ke dalam database relasional (*relational database*) seperti MySQL. RDBMS sendiri tidak diperuntukkan untuk menyimpan data hirarki karena relasi antar table bukanlah relasi hirarki. Namun kita bisa merepresentasikan data hirarki menggunakan relasi ***self-reference***, atau relasi *foreign key* ke table yang sama.
**Data Hirarki** adalah sekumpulan data yang masing-masingnya memiliki satu ***parent*** dengan pengecualian untuk data ***root***. Masing-saing data dalam hirarki juga bisa memilik satu atau beberapa ***child***, atau tidak memiliki child untuk data pada level terendah, yang biasa disebut ***leaf***. Data hirarki yang umumnya terdapat dalam aplikasi perkantoran, seperti data departemen, cabang, kategori barang, chart of accounts, dan sebagainya. Dalam artikel ini kita akan mencoba menggunakan data ***Chart of Accounts*** atau ***Perkiraan*** yang sangat diperlukan dalam sebuah aplikasi akuntansi (*Accounting System*), seperti tampak pada gambar di bawah ini,
![Hirarki Perkiraan (Chart of Account COA Hierarchy](http://pojokprogrammer.net/sites/default/files/pictures/database/hirarki-01.png)
![Hirarki Perkiraan (Chart of Account COA Hierarchy](http://pojokprogrammer.net/sites/default/files/pictures/database/hirarki-02.png)
### Adjacency List Model
**Cara termudah** merepresentasikan data hirarki di dalam database relasional adalah dengan menggunakan model ***Senarai Ketetanggaan*** (***Adjacency List***). dalam model senarai ketetanggaan ini, setiap record memiliki pointer ke *parent* dari record tersebut. Khusus untuk *root* atau data paling atas tidak memiliki parent maka kita set NULL untuk *parent* dari *root*. Untuk contoh kasus kita, maka yang menjadi ***root*** adalah record ***Assets***, ***Liabilities***, dan ***Equities***.
**Keuntungan** penggunaan senarai ketetanggaan adalah kesederhanaannya, karena kita bisa dengan mudah melihat *parent* dari *Petty Cash* adalah *Current Assets*, yang sekaligus merupakan *child* dari *Assets*. Berikut ini adalah DDL dan DML yang kita gunakan sebagai contoh kasus.
CREATE TABLE accounts ( id integer auto_increment, code varchar(255) NOT NULL, name varchar(255) NOT NULL, parent_id integer, PRIMARY KEY (id), FOREIGN KEY (parent_id) REFERENCES accounts (id) ON UPDATE CASCADE ON DELETE RESTRICT ); INSERT INTO accounts (id, code, name, parent_id) VALUES(1000, '1000', 'Assets', NULL); INSERT INTO accounts (id, code, name, parent_id) VALUES(1100, '1100', 'Current Assets', 1000); INSERT INTO accounts (id, code, name, parent_id) VALUES(1110, '1110', 'Petty Cash', 1100); INSERT INTO accounts (id, code, name, parent_id) VALUES(1120, '1120', 'Savings Account', 1100); INSERT INTO accounts (id, code, name, parent_id) VALUES(1130, '1130', 'Deposits Account', 1100); INSERT INTO accounts (id, code, name, parent_id) VALUES(1150, '1150', 'Accounts Receivables', 1100); INSERT INTO accounts (id, code, name, parent_id) VALUES(1200, '1200', 'Fixed Assets', 1000); INSERT INTO accounts (id, code, name, parent_id) VALUES(1210, '1210', 'Furnitures', 1200); INSERT INTO accounts (id, code, name, parent_id) VALUES(1220, '1220', 'Equipments', 1200); INSERT INTO accounts (id, code, name, parent_id) VALUES(1230, '1230', 'Vehicles', 1200); INSERT INTO accounts (id, code, name, parent_id) VALUES(1250, '1250', 'Buildings', 1200); INSERT INTO accounts (id, code, name, parent_id) VALUES(1900, '1900', 'Other Assets', 1000); INSERT INTO accounts (id, code, name, parent_id) VALUES(1910, '1910', 'Patents', 1900); INSERT INTO accounts (id, code, name, parent_id) VALUES(1990, '1990', 'Other Assets', 1900); INSERT INTO accounts (id, code, name, parent_id) VALUES(2000, '2000', 'Liabilities', NULL); INSERT INTO accounts (id, code, name, parent_id) VALUES(2100, '2100', 'Current Liabilities', 2000); INSERT INTO accounts (id, code, name, parent_id) VALUES(2110, '2110', 'Accrued Expenses', 2100); INSERT INTO accounts (id, code, name, parent_id) VALUES(2120, '2120', 'Accounts Payable', 2100); INSERT INTO accounts (id, code, name, parent_id) VALUES(2130, '2130', 'Tax Payable', 2100); INSERT INTO accounts (id, code, name, parent_id) VALUES(2200, '2200', 'Long-Term Liabilities', 2000); INSERT INTO accounts (id, code, name, parent_id) VALUES(2210, '2210', 'Equipments Payable', 2200); INSERT INTO accounts (id, code, name, parent_id) VALUES(2220, '2220', 'Vehicles Payable', 2200); INSERT INTO accounts (id, code, name, parent_id) VALUES(2240, '2240', 'Buildings Payable', 2200); INSERT INTO accounts (id, code, name, parent_id) VALUES(2250, '2250', 'Loans Payable', 2200); INSERT INTO accounts (id, code, name, parent_id) VALUES(3000, '3000', 'Equity', NULL); INSERT INTO accounts (id, code, name, parent_id) VALUES(3100, '3100', 'Capital', 3000); INSERT INTO accounts (id, code, name, parent_id) VALUES(3110, '3110', 'Founders Capital', 3100); INSERT INTO accounts (id, code, name, parent_id) VALUES(3120, '3120', 'Invenstors Capital', 3100); INSERT INTO accounts (id, code, name, parent_id) VALUES(3190, '3190', 'Others Capital', 3100); INSERT INTO accounts (id, code, name, parent_id) VALUES(3200, '3200', 'Earnings', 3000); INSERT INTO accounts (id, code, name, parent_id) VALUES(3210, '3210', 'Retained Earnings', 3200); INSERT INTO accounts (id, code, name, parent_id) VALUES(3220, '3220', 'Current Earnings', 3200);
### Membaca Keseluruhan Hirarki
Hal **yang pasti kita hadapi** saat mengelola data hirarki adalah menampilkan keseluruhan data, biasanya berbentuk ***tree*** dengan tambahan indentasi untuk level yang lebih rendah. Cara paling mudah untuk mencapai hal ini di MySQL adalah dengan menggunakan ***self-join***. Berikut ini **query hirarki** (***hierarchical query***) untuk mendapatkan *tree* ***Assets***
SELECT t1.id, t1.code , t1.name AS name_level1, t2.name as name_level2 , t3.name AS name_level3, t4.name as name_level4 FROM accounts AS t1 LEFT JOIN accounts AS t2 ON t2.parent_id = t1.id LEFT JOIN accounts AS t3 ON t3.parent_id = t2.id LEFT JOIN accounts AS t4 ON t4.parent_id = t3.id WHERE t1.name = 'Assets' ORDER BY t1.code, t2.code, t3.code, t4.code
Hasilnya akan tampak seperti ini
+------+------+-------------+----------------+----------------------+-------------+ | id | code | name_level1 | name_level2 | name_level3 | name_level4 | +------+------+-------------+----------------+----------------------+-------------+ | 1000 | 1000 | Assets | Current Assets | Petty Cash | NULL | | 1000 | 1000 | Assets | Current Assets | Savings Account | NULL | | 1000 | 1000 | Assets | Current Assets | Deposits Account | NULL | | 1000 | 1000 | Assets | Current Assets | Accounts Receivables | NULL | | 1000 | 1000 | Assets | Fixed Assets | Furnitures | NULL | | 1000 | 1000 | Assets | Fixed Assets | Equipments | NULL | | 1000 | 1000 | Assets | Fixed Assets | Vehicles | NULL | | 1000 | 1000 | Assets | Fixed Assets | Buildings | NULL | | 1000 | 1000 | Assets | Other Assets | Patents | NULL | | 1000 | 1000 | Assets | Other Assets | Other Assets | NULL | +------+------+-------------+----------------+----------------------+-------------+ 10 rows in set (0.00 sec)
### Menambahkan *Level* dan *Path*
Jika kita ingin menambahkan kolom yang berisikan level dari masing-masing record, maka kita harus lebih kreatif dalam membangun ***Hierarchical Query*** ini. Karena MySQL belum mendukung ***Common Table Expression*** untuk melakukan ***Recursive Query***, maka yang perlu kita lakukan adalah melakukan query untuk masing-masing level dan kemudian menggabungkannya menggunakan **UNION**. Perhatikan dalam query kali ini kita menggunakan **INNER JOIN**
-- Level 1 SELECT t1.id, t1.code, t1.name, 1 AS level , t1.name AS name_level1, null as name_level2 , null as name_level3, null as name_level4 , t1.code AS path FROM accounts AS t1 WHERE t1.parent_id is null -- Level 2 UNION ALL SELECT t2.id, t2.code, t2.name, 2 AS level , t1.name AS name_level1, t2.name as name_level2 , null as name_level3, null as name_level4 , concat(t1.code,'/',t2.code) AS path FROM accounts AS t1 JOIN accounts AS t2 ON t2.parent_id = t1.id WHERE t1.parent_id is null -- Level 3 UNION ALL SELECT t3.id, t3.code, t3.name, 3 AS level , t1.name AS name_level1, t2.name as name_level2 , t3.name AS name_level3, null as name_level4 , concat(t1.code,'/',t2.code,'/',t3.code) AS path FROM accounts AS t1 JOIN accounts AS t2 ON t2.parent_id = t1.id JOIN accounts AS t3 ON t3.parent_id = t2.id WHERE t1.parent_id is null -- Level 3 UNION ALL SELECT t4.id, t4.code, t4.name, 4 AS level , t1.name AS name_level1, t2.name as name_level2 , t3.name AS name_level3, t4.name as name_level4 , concat(t1.code,'/',t2.code,'/',t3.code,'/',t4.code) AS path FROM accounts AS t1 JOIN accounts AS t2 ON t2.parent_id = t1.id JOIN accounts AS t3 ON t3.parent_id = t2.id JOIN accounts AS t4 ON t4.parent_id = t3.id WHERE t1.parent_id is null -- Ordering Result ORDER BY path
Hasilnya akan tampak seperti ini
+------+------+-----------------------+-------+-------------+-----------------------+----------------------+-------------+----------------+ | id | code | name | level | name_level1 | name_level2 | name_level3 | name_level4 | path | +------+------+-----------------------+-------+-------------+-----------------------+----------------------+-------------+----------------+ | 1000 | 1000 | Assets | 1 | Assets | NULL | NULL | NULL | 1000 | | 1100 | 1100 | Current Assets | 2 | Assets | Current Assets | NULL | NULL | 1000/1100 | | 1110 | 1110 | Petty Cash | 3 | Assets | Current Assets | Petty Cash | NULL | 1000/1100/1110 | | 1120 | 1120 | Savings Account | 3 | Assets | Current Assets | Savings Account | NULL | 1000/1100/1120 | | 1130 | 1130 | Deposits Account | 3 | Assets | Current Assets | Deposits Account | NULL | 1000/1100/1130 | | 1150 | 1150 | Accounts Receivables | 3 | Assets | Current Assets | Accounts Receivables | NULL | 1000/1100/1150 | | 1200 | 1200 | Fixed Assets | 2 | Assets | Fixed Assets | NULL | NULL | 1000/1200 | | 1210 | 1210 | Furnitures | 3 | Assets | Fixed Assets | Furnitures | NULL | 1000/1200/1210 | | 1220 | 1220 | Equipments | 3 | Assets | Fixed Assets | Equipments | NULL | 1000/1200/1220 | | 1230 | 1230 | Vehicles | 3 | Assets | Fixed Assets | Vehicles | NULL | 1000/1200/1230 | | 1250 | 1250 | Buildings | 3 | Assets | Fixed Assets | Buildings | NULL | 1000/1200/1250 | | 1900 | 1900 | Other Assets | 2 | Assets | Other Assets | NULL | NULL | 1000/1900 | | 1910 | 1910 | Patents | 3 | Assets | Other Assets | Patents | NULL | 1000/1900/1910 | | 1990 | 1990 | Other Assets | 3 | Assets | Other Assets | Other Assets | NULL | 1000/1900/1990 | | 2000 | 2000 | Liabilities | 1 | Liabilities | NULL | NULL | NULL | 2000 | | 2100 | 2100 | Current Liabilities | 2 | Liabilities | Current Liabilities | NULL | NULL | 2000/2100 | | 2110 | 2110 | Accrued Expenses | 3 | Liabilities | Current Liabilities | Accrued Expenses | NULL | 2000/2100/2110 | | 2120 | 2120 | Accounts Payable | 3 | Liabilities | Current Liabilities | Accounts Payable | NULL | 2000/2100/2120 | | 2130 | 2130 | Tax Payable | 3 | Liabilities | Current Liabilities | Tax Payable | NULL | 2000/2100/2130 | | 2200 | 2200 | Long-Term Liabilities | 2 | Liabilities | Long-Term Liabilities | NULL | NULL | 2000/2200 | | 2210 | 2210 | Equipments Payable | 3 | Liabilities | Long-Term Liabilities | Equipments Payable | NULL | 2000/2200/2210 | | 2220 | 2220 | Vehicles Payable | 3 | Liabilities | Long-Term Liabilities | Vehicles Payable | NULL | 2000/2200/2220 | | 2240 | 2240 | Buildings Payable | 3 | Liabilities | Long-Term Liabilities | Buildings Payable | NULL | 2000/2200/2240 | | 2250 | 2250 | Loans Payable | 3 | Liabilities | Long-Term Liabilities | Loans Payable | NULL | 2000/2200/2250 | | 3000 | 3000 | Equity | 1 | Equity | NULL | NULL | NULL | 3000 | | 3100 | 3100 | Capital | 2 | Equity | Capital | NULL | NULL | 3000/3100 | | 3110 | 3110 | Founders Capital | 3 | Equity | Capital | Founders Capital | NULL | 3000/3100/3110 | | 3120 | 3120 | Invenstors Capital | 3 | Equity | Capital | Invenstors Capital | NULL | 3000/3100/3120 | | 3190 | 3190 | Others Capital | 3 | Equity | Capital | Others Capital | NULL | 3000/3100/3190 | | 3200 | 3200 | Earnings | 2 | Equity | Earnings | NULL | NULL | 3000/3200 | | 3210 | 3210 | Retained Earnings | 3 | Equity | Earnings | Retained Earnings | NULL | 3000/3200/3210 | | 3220 | 3220 | Current Earnings | 3 | Equity | Earnings | Current Earnings | NULL | 3000/3200/3220 | +------+------+-----------------------+-------+-------------+-----------------------+----------------------+-------------+----------------+ 32 rows in set (0.01 sec)
### Mencari Semua *Leaf Node*
Kita juga bisa mendapatkan semua ***leaf node***, yaitu node pada level terbawah dan tidak memiliki **child**, dengan menggunakan **LEFT JOIN**
-- Level 1 SELECT t1.id, t1.code , t1.name AS name_level1, t2.name as name_level2 , t3.name AS name_level3, t4.name as name_level4 FROM accounts AS t1 LEFT JOIN accounts AS t2 ON t2.parent_id = t1.id LEFT JOIN accounts AS t3 ON t3.parent_id = t2.id LEFT JOIN accounts AS t4 ON t4.parent_id = t3.id WHERE t1.parent_id is null ORDER BY t1.code, t2.code, t3.code, t4.code
Hasilnya adaah seperti ini.
+------+------+-------------+-----------------------+----------------------+-------------+ | id | code | name_level1 | name_level2 | name_level3 | name_level4 | +------+------+-------------+-----------------------+----------------------+-------------+ | 1000 | 1000 | Assets | Current Assets | Petty Cash | NULL | | 1000 | 1000 | Assets | Current Assets | Savings Account | NULL | | 1000 | 1000 | Assets | Current Assets | Deposits Account | NULL | | 1000 | 1000 | Assets | Current Assets | Accounts Receivables | NULL | | 1000 | 1000 | Assets | Fixed Assets | Furnitures | NULL | | 1000 | 1000 | Assets | Fixed Assets | Equipments | NULL | | 1000 | 1000 | Assets | Fixed Assets | Vehicles | NULL | | 1000 | 1000 | Assets | Fixed Assets | Buildings | NULL | | 1000 | 1000 | Assets | Other Assets | Patents | NULL | | 1000 | 1000 | Assets | Other Assets | Other Assets | NULL | | 2000 | 2000 | Liabilities | Current Liabilities | Accrued Expenses | NULL | | 2000 | 2000 | Liabilities | Current Liabilities | Accounts Payable | NULL | | 2000 | 2000 | Liabilities | Current Liabilities | Tax Payable | NULL | | 2000 | 2000 | Liabilities | Long-Term Liabilities | Equipments Payable | NULL | | 2000 | 2000 | Liabilities | Long-Term Liabilities | Vehicles Payable | NULL | | 2000 | 2000 | Liabilities | Long-Term Liabilities | Buildings Payable | NULL | | 2000 | 2000 | Liabilities | Long-Term Liabilities | Loans Payable | NULL | | 3000 | 3000 | Equity | Capital | Founders Capital | NULL | | 3000 | 3000 | Equity | Capital | Invenstors Capital | NULL | | 3000 | 3000 | Equity | Capital | Others Capital | NULL | | 3000 | 3000 | Equity | Earnings | Retained Earnings | NULL | | 3000 | 3000 | Equity | Earnings | Current Earnings | NULL | +------+------+-------------+-----------------------+----------------------+-------------+ 22 rows in set (0.00 sec)
### Kesimpulan
Meskipun **terlihat mudah**, ada beberapa hal yang perlu kita perhatikan dalam mengelola data hirarki di MySQL ini,
– Kita harus mengetahui jumlah level sejak awal
– Setiap ada penambahan level, maka kita juga harus menambahkan ***left join*** lagi ke dalam query. Perlu diketahui bahwa semakin banyak *left join* maka kinerja (*performance*) mysql akan menurun.
– Penghapusan record (*node*) tidak boleh dilakukan secara sembarangan karena ada resiko tree akan berantakan jika ada child yang mengacu ke node yang dihapus. Namun hal ini bisa diatasi dengan ***Foreign Key Constraint***.
Cara lain untuk mengembangkan query hirarki ini adalah dengan menggunakan ***stored procedure*** untuk membangun query secara dinamis sehingga bisa mengantisipasi jumlah level yang lebeih banyak dan bahkan tidak terbatas.
Semoga berguna!
.