Skema dari Bagian 1 hanyalah kerangka; denyut aplikasi manajemen tugas dimulai saat anggota tim baru bergabung dan tugas baru dibuat. Pada bagian ini kita mengisi tugas_db dengan data awal, lalu melakukan pekerjaan sehari-hari papan tugas: mendaftarkan pengguna baru, membuat tugas untuknya, mengalihkan tugas yang belum diambil, dan membersihkan tugas yang keliru dibuat. Sepanjang jalan, tiga jenis constraint — UNIQUE, foreign key, dan ENUM — akan berkesempatan menunjukkan cara kerjanya menolak data yang salah bentuk.
Membangun Ulang Kondisi Awal #
Setiap bagian proyek ini mandiri, jadi blok pertama kita selalu membangun ulang tugas_db dari nol: skema dari berkas database/skema.sql diikuti seluruh isi database/data-awal.sql:
DROP DATABASE IF EXISTS tugas_db;
CREATE DATABASE tugas_db;
USE tugas_db;
CREATE TABLE pengguna (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
email VARCHAR(60) NOT NULL UNIQUE
);
CREATE TABLE proyek (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
pemilik_id INT NOT NULL,
CONSTRAINT fk_proyek_pemilik
FOREIGN KEY (pemilik_id) REFERENCES pengguna (id)
);
CREATE TABLE tugas (
id INT AUTO_INCREMENT PRIMARY KEY,
judul VARCHAR(80) NOT NULL,
proyek_id INT NOT NULL,
assignee_id INT NULL,
prioritas ENUM('rendah', 'sedang', 'tinggi') NOT NULL,
tenggat DATE NULL,
status ENUM('baru', 'berjalan', 'selesai') NOT NULL,
CONSTRAINT fk_tugas_proyek
FOREIGN KEY (proyek_id) REFERENCES proyek (id),
CONSTRAINT fk_tugas_assignee
FOREIGN KEY (assignee_id) REFERENCES pengguna (id)
);
CREATE TABLE riwayat_status (
id INT AUTO_INCREMENT PRIMARY KEY,
tugas_id INT NOT NULL,
status_lama ENUM('baru', 'berjalan', 'selesai') NOT NULL,
status_baru ENUM('baru', 'berjalan', 'selesai') NOT NULL,
waktu DATETIME NOT NULL,
catatan VARCHAR(200),
CONSTRAINT fk_riwayat_tugas
FOREIGN KEY (tugas_id) REFERENCES tugas (id)
);
INSERT INTO pengguna (id, nama, email) VALUES
(1, 'Budi Santoso', '[email protected]'),
(2, 'Siti Rahma', '[email protected]'),
(3, 'Andi Wijaya', '[email protected]'),
(4, 'Dewi Lestari', '[email protected]'),
(5, 'Tono Pratama', '[email protected]'),
(6, 'Rina Kusuma', '[email protected]');
INSERT INTO proyek (id, nama, pemilik_id) VALUES
(1, 'Rilis Aplikasi Mobile', 1),
(2, 'Migrasi Data Gudang', 2),
(3, 'Kampanye Pemasaran Q1', 4);
INSERT INTO tugas (id, judul, proyek_id, assignee_id, prioritas, tenggat, status) VALUES
(1, 'Desain mockup halaman utama', 1, 3, 'tinggi', '2026-02-10', 'selesai'),
(2, 'Implementasi API autentikasi', 1, 1, 'tinggi', '2026-02-10', 'berjalan'),
(3, 'Uji coba beta di Play Store', 1, NULL, 'sedang', '2026-03-15', 'baru'),
(4, 'Pemetaan tabel lama ke baru', 2, 2, 'tinggi', '2026-02-05', 'selesai'),
(5, 'Skrip migrasi batch pertama', 2, 5, 'sedang', '2026-02-25', 'berjalan'),
(6, 'Validasi hasil migrasi', 2, 4, 'rendah', '2026-03-01', 'baru'),
(7, 'Menyusun materi iklan', 3, 4, 'sedang', '2026-02-12', 'selesai'),
(8, 'Riset kata kunci iklan', 3, 3, 'rendah', '2026-02-08', 'selesai'),
(9, 'Negosiasi anggaran media', 3, 1, 'tinggi', '2026-02-12', 'berjalan'),
(10, 'Laporan performa kampanye', 3, NULL, 'sedang', '2026-04-10', 'baru');
INSERT INTO riwayat_status (id, tugas_id, status_lama, status_baru, waktu, catatan) VALUES
(1, 1, 'baru', 'berjalan', '2026-01-06 10:15:00', 'Mulai kerjakan mockup'),
(2, 1, 'berjalan', 'selesai', '2026-02-09 16:40:00', 'Mockup disetujui tim'),
(3, 2, 'baru', 'berjalan', '2026-01-20 11:00:00', 'Sprint 1 dimulai'),
(4, 4, 'baru', 'berjalan', '2026-01-07 09:00:00', 'Pemetaan awal berjalan'),
(5, 4, 'berjalan', 'selesai', '2026-02-04 17:00:00', 'Pemetaan tervalidasi'),
(6, 5, 'baru', 'berjalan', '2026-01-15 10:30:00', 'Skrip versi awal mulai dikerjakan'),
(7, 7, 'baru', 'berjalan', '2026-01-12 10:00:00', 'Brief iklan diterima'),
(8, 7, 'berjalan', 'selesai', '2026-02-11 15:30:00', 'Materi disetujui manajemen'),
(9, 8, 'baru', 'berjalan', '2026-01-13 09:10:00', 'Riset kata kunci dimulai'),
(10, 8, 'berjalan', 'selesai', '2026-02-06 12:00:00', 'Riset selesai, 40 kata kunci'),
(11, 9, 'baru', 'berjalan', '2026-01-25 10:30:00', 'Rapat awal dengan vendor');Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
Query OK, 10 rows affected (0.00 sec)
Records: 10 Duplicates: 0 Warnings: 0
Query OK, 11 rows affected (0.00 sec)
Records: 11 Duplicates: 0 Warnings: 0Menambah Pengguna Baru #
Tim kedatangan anggota baru: Fajar Nugroho. Kita tidak menyebut kolom id — biarkan AUTO_INCREMENT melanjutkan dari enam baris awal sehingga Fajar menjadi pengguna ketujuh:
INSERT INTO pengguna (nama, email)
VALUES ('Fajar Nugroho', '[email protected]');Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Kolom yang tidak disebut pada daftar INSERT mengisi nilai defaultnya; untuk id artinya angka berikutnya dari penghitung AUTO_INCREMENT, yaitu 7. Email yang sama persis tidak akan lolos dua kali — coba daftarkan pengguna lain memakai email milik Budi:
-- @error
INSERT INTO pengguna (nama, email) VALUES ('Budi Kedua', '[email protected]');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'pengguna.email'
ByePesan galat menyebut pengguna.email — nama index unik yang dibuat kolom UNIQUE — lengkap dengan nilai yang bentrok. Dua akun untuk satu orang adalah sumber kekacauan klasik aplikasi tim, dan database menolaknya di muka pintu.
Membuat Tugas untuk Pengguna Baru #
Fajar langsung mendapat tugas pertamanya di proyek Rilis Aplikasi Mobile. assignee_id kita isi 7 — nomor id Fajar yang barusan terbit:
INSERT INTO tugas (judul, proyek_id, assignee_id, prioritas, tenggat, status)
VALUES ('Dokumentasi API publik', 1, 7, 'sedang', '2026-03-20', 'baru');Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Foreign key kini ikut bekerja. Coba buat tugas yang menunjuk proyek bernomor 99 — nomor yang tidak ada di tabel proyek:
-- @error
INSERT INTO tugas (judul, proyek_id, assignee_id, prioritas, tenggat, status) VALUES ('Tugas siluman', 99, NULL, 'sedang', '2026-03-01', 'baru');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`tugas_db`.`tugas`, CONSTRAINT `fk_tugas_proyek` FOREIGN KEY (`proyek_id`) REFERENCES `proyek` (`id`))
ByeGalat 1452 muncul dari constraint fk_tugas_proyek: baris anak tidak boleh menunjuk induk yang tidak ada. Pesan lengkapnya menyebut nama constraint, tabel, dan kolom yang gagal divalidasi — keuntungan menamai foreign key secara eksplisit sejak Bagian 1. Perhatikan pula bahwa assignee_id pada percobaan tadi dibiarkan NULL dan tidak digugat: nilai NULL tidak pernah diperiksa foreign key, sehingga tugas tanpa pengerja tetap sah.
Mengalihkan Penanggung Jawab #
Tugas 3 (uji coba beta) masih NULL penanggungnya. Rapat mingguan memutuskan Dewi Lestari yang mengambil alih — satu UPDATE mengubah kolom tersebut:
UPDATE tugas SET assignee_id = 4
WHERE id = 3;
SELECT
t.id,
t.judul,
u.nama AS penanggung_jawab,
t.status
FROM tugas t
LEFT JOIN pengguna u ON u.id = t.assignee_id
WHERE t.id = 3;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+----+-----------------------------+------------------+--------+
| id | judul | penanggung_jawab | status |
+----+-----------------------------+------------------+--------+
| 3 | Uji coba beta di Play Store | Dewi Lestari | baru |
+----+-----------------------------+------------------+--------+
1 row in set (0.00 sec)Kolom penanggung_jawab yang tadinya NULL kini terisi Dewi Lestari — bukti bahwa UPDATE mengubah satu baris yang dipilih WHERE id = 3. WHERE pada UPDATE wajib dipikirkan matang: tanpa klausa itu, seluruh baris tabel akan terubah sekaligus.
Menghapus Tugas — dan Ditolak Riwayatnya #
Tugas 11 keliru dibuat (dokumentasinya sudah ada di wiki tim), jadi kita hapus. Tetapi ada satu tugas yang tidak boleh terhapus: tugas 1 sudah punya dua baris riwayat, dan database wajib menjaga sejarahnya:
-- @error
DELETE FROM tugas WHERE id = 1;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`tugas_db`.`riwayat_status`, CONSTRAINT `fk_riwayat_tugas` FOREIGN KEY (`tugas_id`) REFERENCES `tugas` (`id`))
ByeGalat 1451 menjelaskan arah kebalikannya dari 1452: kita mencoba menghapus baris induk yang masih dirujuk baris anak di riwayat_status. Riwayat tanpa tugas induknya menjadi catatan yatim — tidak lagi bisa dipercaya. InnoDB memilih menolak penghapusan daripada membiarkan data setengah hidup. Tugas 11 aman dihapus karena belum punya riwayat apa pun:
DELETE FROM tugas WHERE id = 11;
SELECT COUNT(*) AS total_tugas FROM tugas;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
+-------------+
| total_tugas |
+-------------+
| 10 |
+-------------+
1 row in set (0.00 sec)Sepuluh tugas kembali — penghapusan tugas 11 hanya mengurangi satu, dan baris riwayat tidak tersentuh karena tugas itu memang belum pernah berpindah status.
Membersihkan Database Latihan #
Bagian ini selesai dan tugas_db boleh kita bongkar:
DROP DATABASE IF EXISTS tugas_db;Output yang Dihasilkan:
Query OK, 4 rows affected (0.00 sec)Referensi #
- INSERT Statement — MySQL 8.4 Reference Manual
- UPDATE Statement — MySQL 8.4 Reference Manual
- DELETE Statement — MySQL 8.4 Reference Manual
CRUD dasar sudah berjalan dengan tiga lapis pengaman yang terbukti hidup. Bagian 3 memasuki jantung aplikasi: mengubah status tugas beserta pencatatannya dalam satu transaksi — dan melihat betapa berantakannya dunia bila keduanya dipisahkan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
database/data-awal.sql
Unduh berkas-- data-awal.sql — Proyek: Database Manajemen Tugas
-- Data contoh deterministik (6 pengguna, 3 proyek, 10 tugas, riwayat awal).
-- Cara pakai: mysql -u root -p tugas_db < data-awal.sql
-- (jalankan skema.sql terlebih dahulu)
USE tugas_db;
INSERT INTO pengguna (id, nama, email) VALUES
(1, 'Budi Santoso', '[email protected]'),
(2, 'Siti Rahma', '[email protected]'),
(3, 'Andi Wijaya', '[email protected]'),
(4, 'Dewi Lestari', '[email protected]'),
(5, 'Tono Pratama', '[email protected]'),
(6, 'Rina Kusuma', '[email protected]');
INSERT INTO proyek (id, nama, pemilik_id) VALUES
(1, 'Rilis Aplikasi Mobile', 1),
(2, 'Migrasi Data Gudang', 2),
(3, 'Kampanye Pemasaran Q1', 4);
INSERT INTO tugas (id, judul, proyek_id, assignee_id, prioritas, tenggat, status) VALUES
(1, 'Desain mockup halaman utama', 1, 3, 'tinggi', '2026-02-10', 'selesai'),
(2, 'Implementasi API autentikasi', 1, 1, 'tinggi', '2026-02-10', 'berjalan'),
(3, 'Uji coba beta di Play Store', 1, NULL, 'sedang', '2026-03-15', 'baru'),
(4, 'Pemetaan tabel lama ke baru', 2, 2, 'tinggi', '2026-02-05', 'selesai'),
(5, 'Skrip migrasi batch pertama', 2, 5, 'sedang', '2026-02-25', 'berjalan'),
(6, 'Validasi hasil migrasi', 2, 4, 'rendah', '2026-03-01', 'baru'),
(7, 'Menyusun materi iklan', 3, 4, 'sedang', '2026-02-12', 'selesai'),
(8, 'Riset kata kunci iklan', 3, 3, 'rendah', '2026-02-08', 'selesai'),
(9, 'Negosiasi anggaran media', 3, 1, 'tinggi', '2026-02-12', 'berjalan'),
(10, 'Laporan performa kampanye', 3, NULL, 'sedang', '2026-04-10', 'baru');
INSERT INTO riwayat_status (id, tugas_id, status_lama, status_baru, waktu, catatan) VALUES
(1, 1, 'baru', 'berjalan', '2026-01-06 10:15:00', 'Mulai kerjakan mockup'),
(2, 1, 'berjalan', 'selesai', '2026-02-09 16:40:00', 'Mockup disetujui tim'),
(3, 2, 'baru', 'berjalan', '2026-01-20 11:00:00', 'Sprint 1 dimulai'),
(4, 4, 'baru', 'berjalan', '2026-01-07 09:00:00', 'Pemetaan awal berjalan'),
(5, 4, 'berjalan', 'selesai', '2026-02-04 17:00:00', 'Pemetaan tervalidasi'),
(6, 5, 'baru', 'berjalan', '2026-01-15 10:30:00', 'Skrip versi awal mulai dikerjakan'),
(7, 7, 'baru', 'berjalan', '2026-01-12 10:00:00', 'Brief iklan diterima'),
(8, 7, 'berjalan', 'selesai', '2026-02-11 15:30:00', 'Materi disetujui manajemen'),
(9, 8, 'baru', 'berjalan', '2026-01-13 09:10:00', 'Riset kata kunci dimulai'),
(10, 8, 'berjalan', 'selesai', '2026-02-06 12:00:00', 'Riset selesai, 40 kata kunci'),
(11, 9, 'baru', 'berjalan', '2026-01-25 10:30:00', 'Rapat awal dengan vendor');
database/skema.sql
Unduh berkas-- skema.sql — Proyek: Database Manajemen Tugas
-- Membangun database tugas_db beserta 4 tabel berelasi (pengguna, proyek,
-- tugas, riwayat_status) lengkap dengan foreign key bernama.
-- Cara pakai: mysql -u root -p < skema.sql
DROP DATABASE IF EXISTS tugas_db;
CREATE DATABASE tugas_db;
USE tugas_db;
CREATE TABLE pengguna (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
email VARCHAR(60) NOT NULL UNIQUE
);
CREATE TABLE proyek (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
pemilik_id INT NOT NULL,
CONSTRAINT fk_proyek_pemilik
FOREIGN KEY (pemilik_id) REFERENCES pengguna (id)
);
CREATE TABLE tugas (
id INT AUTO_INCREMENT PRIMARY KEY,
judul VARCHAR(80) NOT NULL,
proyek_id INT NOT NULL,
assignee_id INT NULL,
prioritas ENUM('rendah', 'sedang', 'tinggi') NOT NULL,
tenggat DATE NULL,
status ENUM('baru', 'berjalan', 'selesai') NOT NULL,
CONSTRAINT fk_tugas_proyek
FOREIGN KEY (proyek_id) REFERENCES proyek (id),
CONSTRAINT fk_tugas_assignee
FOREIGN KEY (assignee_id) REFERENCES pengguna (id)
);
CREATE TABLE riwayat_status (
id INT AUTO_INCREMENT PRIMARY KEY,
tugas_id INT NOT NULL,
status_lama ENUM('baru', 'berjalan', 'selesai') NOT NULL,
status_baru ENUM('baru', 'berjalan', 'selesai') NOT NULL,
waktu DATETIME NOT NULL,
catatan VARCHAR(200),
CONSTRAINT fk_riwayat_tugas
FOREIGN KEY (tugas_id) REFERENCES tugas (id)
);
queries/laporan-beban-kerja.sql
Unduh berkas-- laporan-beban-kerja.sql — Beban kerja tiap anggota (total/selesai/berjalan/baru).
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-beban-kerja.sql
USE tugas_db;
SELECT
p.nama,
COUNT(t.id) AS total_tugas,
SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) AS selesai,
SUM(CASE WHEN t.status = 'berjalan' THEN 1 ELSE 0 END) AS berjalan,
SUM(CASE WHEN t.status = 'baru' THEN 1 ELSE 0 END) AS baru
FROM pengguna p
LEFT JOIN tugas t ON t.assignee_id = p.id
GROUP BY p.id, p.nama
ORDER BY total_tugas DESC, p.nama;
queries/laporan-overdue.sql
Unduh berkas-- laporan-overdue.sql — Tugas menembus tenggat namun belum selesai.
-- Ganti tanggal acuan sesuai kebutuhan (di aplikasi: CURRENT_DATE()).
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-overdue.sql
USE tugas_db;
SELECT
t.id,
t.judul,
pr.nama AS proyek,
p.nama AS penanggung,
t.tenggat,
DATEDIFF('2026-02-15', t.tenggat) AS hari_terlambat
FROM tugas t
JOIN proyek pr ON pr.id = t.proyek_id
LEFT JOIN pengguna p ON p.id = t.assignee_id
WHERE t.status <> 'selesai'
AND t.tenggat < '2026-02-15'
ORDER BY t.tenggat, t.id;
queries/laporan-status-proyek.sql
Unduh berkas-- laporan-status-proyek.sql — Distribusi status + persentase selesai per proyek.
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-status-proyek.sql
USE tugas_db;
SELECT
pr.nama AS proyek,
COUNT(t.id) AS total_tugas,
SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) AS selesai,
ROUND(
SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) / COUNT(t.id) * 100,
1
) AS persen_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY persen_selesai DESC, pr.nama;
tests/pemeriksaan.sql
Unduh berkas-- pemeriksaan.sql — Proyek: Database Manajemen Tugas
-- Rangkaian pemeriksaan otomatis: skema, jumlah data, hasil alur Bagian 3,
-- dan angka laporan Bagian 4. Ekspektasi tertulis di komentar tiap blok.
-- Cara pakai (urutan lengkap membangun kondisi akhir proyek):
-- mysql -u root -p < database/skema.sql
-- mysql -u root -p < database/data-awal.sql (kondisi awal Bagian 2)
-- ...jalankan alur transaksi Bagian 3... (atau lihat Bagian 4)
-- mysql -u root -p < tests/pemeriksaan.sql
-- Pemeriksaan ini mengasumsikan kondisi AKHIR (13 baris riwayat).
USE tugas_db;
-- [1] Struktur: 4 tabel harus ada
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'tugas_db'
ORDER BY table_name;
-- Ekspektasi: pengguna, proyek, riwayat_status, tugas (4 baris)
-- [2] Jumlah data kondisi akhir: 6 pengguna, 3 proyek, 10 tugas, 13 riwayat
SELECT
(SELECT COUNT(*) FROM pengguna) AS pengguna,
(SELECT COUNT(*) FROM proyek) AS proyek,
(SELECT COUNT(*) FROM tugas) AS tugas,
(SELECT COUNT(*) FROM riwayat_status) AS riwayat;
-- Ekspektasi: 6, 3, 10, 13
-- [3] Integritas ENUM: email unik dijaga (duplikat ditolak)
-- (dibuktikan di Bagian 2 dengan ERROR 1062; di sini cukup hitung unik)
SELECT COUNT(DISTINCT email) AS email_unik FROM pengguna;
-- Ekspektasi: 6
-- [4] Alur Bagian 3 terekam: tugas 3 kini milik Rina dan berjalan
SELECT judul, assignee_id, status FROM tugas WHERE id = 3;
-- Ekspektasi: Uji coba beta di Play Store | 6 | berjalan
-- [5] Riwayat tugas 3: tepat 1 baris baru → berjalan
SELECT COUNT(*) AS perubahan_tugas3 FROM riwayat_status WHERE tugas_id = 3;
-- Ekspektasi: 1
-- [6] Durasi siklus tugas 2: selesai 19 hari setelah mulai
SELECT
TIMESTAMPDIFF(DAY, MIN(waktu), MAX(waktu)) AS durasi_hari
FROM riwayat_status
WHERE tugas_id = 2;
-- Ekspektasi: 19
-- [7] Laporan overdue (acuan 2026-02-15): 4 tugas belum selesai menembus tenggat
SELECT COUNT(*) AS tugas_terlambat
FROM tugas
WHERE status <> 'selesai' AND tenggat < '2026-02-15';
-- Ekspektasi: 4
-- [8] Laporan status proyek: Kampanye Pemasaran Q1 paling tinggi persentase selesainya
SELECT
pr.nama AS proyek,
ROUND(
SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) / COUNT(t.id) * 100,
1
) AS persen_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY persen_selesai DESC;
-- Ekspektasi: Kampanye Pemasaran Q1 (83.3) teratas, lalu Migrasi Data Gudang, Rilis Aplikasi Mobile