Tabel karyawan menyimpan dua jenis informasi sekaligus: data operasional seperti nama dan departemen yang dibutuhkan banyak orang, serta data sensitif seperti gaji yang seharusnya sempit pembaginya. Bila seluruh tim diberi akses penuh ke tabel, kolom gaji ikut terbaca oleh semua orang; bila akses ditolak semua, pekerjaan operasional macet. VIEW menyelesaikan kebuntuan ini dengan menjadi pintu data yang kolom, baris, dan hak aksesnya kita rancang sendiri. Pada chapter ini kita bangun view yang menyembunyikan kolom gaji, menjaga isi view dengan WITH CHECK OPTION, membedah jebakan view tanpa check option, lalu memberikan akses granular kepada user lain lewat GRANT — tanpa pernah membuka tabel dasarnya.
Menyiapkan Tabel Latihan #
Kita pakai skema latihan yang sama seperti chapter-chapter sebelumnya — database belajar_view dengan tabel karyawan delapan baris:
DROP DATABASE IF EXISTS belajar_view;
CREATE DATABASE belajar_view;
USE belajar_view;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE karyawan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
gaji DECIMAL(10,2) NOT NULL,
departemen VARCHAR(30) NOT NULL,
tanggal_masuk DATE NOT NULL
);
INSERT INTO karyawan (nama, gaji, departemen, tanggal_masuk) VALUES
('Budi Santoso', 8500000.00, 'IT', '2021-03-01'),
('Siti Rahayu', 7200000.00, 'IT', '2022-07-15'),
('Andi Wijaya', 9500000.00, 'Keuangan', '2020-01-20'),
('Dewi Lestari', 6800000.00, 'Keuangan', '2023-02-10'),
('Rina Kurnia', 5500000.00, 'HR', '2023-09-05'),
('Tono Prasetyo', 7700000.00, 'IT', '2021-11-01'),
('Sari Wulandari', 6300000.00, 'HR', '2024-01-08'),
('Joko Susilo', 8900000.00, 'Pemasaran', '2022-04-12');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0Menyembunyikan Kolom Sensitif dengan View #
Mekanisme penyembunyian paling dasar bekerja lewat daftar kolom definisi: hanya kolom yang kita sebut di SELECT yang tersedia melalui view. Kita buat view publik untuk konsumsi umum — tanpa kolom gaji:
CREATE VIEW v_karyawan_publik AS
SELECT id, nama, departemen, tanggal_masuk FROM karyawan;
SELECT * FROM v_karyawan_publik ORDER BY id;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+----+----------------+------------+---------------+
| id | nama | departemen | tanggal_masuk |
+----+----------------+------------+---------------+
| 1 | Budi Santoso | IT | 2021-03-01 |
| 2 | Siti Rahayu | IT | 2022-07-15 |
| 3 | Andi Wijaya | Keuangan | 2020-01-20 |
| 4 | Dewi Lestari | Keuangan | 2023-02-10 |
| 5 | Rina Kurnia | HR | 2023-09-05 |
| 6 | Tono Prasetyo | IT | 2021-11-01 |
| 7 | Sari Wulandari | HR | 2024-01-08 |
| 8 | Joko Susilo | Pemasaran | 2022-04-12 |
+----+----------------+------------+---------------+
8 rows in set (0.00 sec)Hasilnya sebuah “tabel karyawan tanpa gaji”: delapan baris lengkap dengan kolom id, nama, departemen, dan tanggal_masuk. Mari kita bedah mengapa penyembunyian ini efektif: definisi view menyalin daftar kolom secara eksplisit pada saat CREATE VIEW dijalankan, dan daftar itulah satu-satunya jendela yang tersedia — pembaca view tidak punya cara menuliskan SELECT gaji FROM v_karyawan_publik karena kolom gaji memang tidak ada di dalam view, bukan sekadar disembunyikan dari tampilan. Data gaji tetap utuh di tabel karyawan dan tetap terbaca oleh user yang memang berhak — view hanya memotong jalur akses bagi yang tidak berhak.
WITH CHECK OPTION: Menjaga Baris Tetap di Dalam View #
View bertipe penyaring tidak hanya dibaca — pada kondisi tertentu juga bisa menerima INSERT dan UPDATE (aturan lengkapnya dibahas di chapter batasan view). Tanpa pengaman, perubahan lewat view bisa saja menulis baris yang langsung lolos dari filter view itu sendiri. Klausa WITH CHECK OPTION menutup celah tersebut. Kita buat view pegawai IT lengkap dengan pengaman:
CREATE VIEW v_karyawan_it AS
SELECT id, nama, gaji, departemen, tanggal_masuk
FROM karyawan WHERE departemen = 'IT'
WITH CHECK OPTION;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)Mekanisme kerjanya begini: setiap kali INSERT atau UPDATE menyentuh baris melalui view ini, server menghitung ulang klausa WHERE departemen = 'IT' terhadap baris hasil akhir; baris yang tidak lolos filter ditolak sebelum disimpan. Kita buktikan dengan menyisipkan pegawai yang departemennya bukan IT:
-- @error
INSERT INTO v_karyawan_it (nama, gaji, departemen, tanggal_masuk) VALUES ('Uji Coba', 5000000.00, 'HR', '2026-01-05');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1369 (HY000): CHECK OPTION failed 'belajar_view.v_karyawan_it'
ByeServer menolak dengan ERROR 1369 (HY000): CHECK OPTION failed 'belajar_view.v_karyawan_it' — baris calon pegawai berdepartemen HR gagal lolos pemeriksaan departemen = 'IT', sehingga penyisipan dibatalkan dan tabel dasar tidak berubah. Sebaliknya, baris yang memenuhi filter tetap lolos secara normal:
INSERT INTO v_karyawan_it (nama, gaji, departemen, tanggal_masuk)
VALUES ('Farhan Maulana', 6000000.00, 'IT', '2026-01-10');
SELECT id, nama, departemen FROM v_karyawan_it ORDER BY id;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
+----+----------------+------------+
| id | nama | departemen |
+----+----------------+------------+
| 1 | Budi Santoso | IT |
| 2 | Siti Rahayu | IT |
| 6 | Tono Prasetyo | IT |
| 9 | Farhan Maulana | IT |
+----+----------------+------------+
4 rows in set (0.00 sec)Pegawai baru tersimpan ke tabel karyawan dengan id 9 dan langsung terlihat di view karena departemennya IT. Pemeriksaan yang sama berlaku dua arah: UPDATE yang akan mengeluarkan baris dari view juga ditolak:
-- @error
UPDATE v_karyawan_it SET departemen = 'HR' WHERE id = 1;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1369 (HY000): CHECK OPTION failed 'belajar_view.v_karyawan_it'
ByeERROR 1369 muncul lagi karena hasil akhir baris — Budi Santoso dipindahkan ke HR — tidak lagi memenuhi departemen = 'IT'. Sementara itu pembaruan yang tidak menyentuh kolom filter tetap sah; gaji Budi kita naikkan lewat view yang sama:
UPDATE v_karyawan_it SET gaji = 8600000.00 WHERE id = 1;
SELECT id, nama, gaji FROM karyawan WHERE id = 1;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+----+--------------+------------+
| id | nama | gaji |
+----+--------------+------------+
| 1 | Budi Santoso | 8600000.00 |
+----+--------------+------------+
1 row in set (0.00 sec)Nilai gaji baris Budi di tabel dasar berubah menjadi 8600000.00 — bukti bahwa CHECK OPTION tidak menghalangi pembaruan yang sah, ia hanya menjaga agar baris yang dipegang view tetap berada di dalam jangkauan filternya.
Tanpa CHECK OPTION: Baris Hantu #
Untuk melihat apa yang dicegah CHECK OPTION, kita bangun kembaran view IT tanpa klausa pengaman:
CREATE VIEW v_karyawan_it_longgar AS
SELECT id, nama, gaji, departemen, tanggal_masuk
FROM karyawan WHERE departemen = 'IT';Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)INSERT INTO v_karyawan_it_longgar (nama, gaji, departemen, tanggal_masuk)
VALUES ('Salah Pindah', 5750000.00, 'HR', '2026-02-01');
SELECT COUNT(*) AS baris_terlihat FROM v_karyawan_it_longgar;
SELECT id, nama, departemen FROM karyawan WHERE departemen = 'HR' ORDER BY id;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
+----------------+
| baris_terlihat |
+----------------+
| 4 |
+----------------+
1 row in set (0.00 sec)
+----+----------------+------------+
| id | nama | departemen |
+----+----------------+------------+
| 5 | Rina Kurnia | HR |
| 7 | Sari Wulandari | HR |
| 10 | Salah Pindah | HR |
+----+----------------+------------+
3 rows in set (0.00 sec)Penyisipan pelanggar filter kali ini sukses — Query OK, 1 row affected — karena tidak ada pemeriksaan yang menolaknya. Akibatnya, baris ke-10 bernama Salah Pindah hidup di tabel karyawan (terlihat pada query departemen = 'HR' yang mengembalikan tiga baris), tetapi view v_karyawan_it_longgar tetap melaporkan empat baris: baris baru itu tidak pernah tampil di view yang dipakai untuk menyisipkannya. Inilah yang kita sebut baris hantu: data tersimpan, aplikasi yang bekerja lewat view mengira gagal, dan laporan keuangan pun bisa berbeda dari laporan aplikasi. Baris uji ini kita bereskan dulu:
DELETE FROM karyawan WHERE nama = 'Salah Pindah';Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Akses Granular dengan GRANT #
Penyembunyian kolom dan CHECK OPTION baru bermakna penuh bila user memang hanya bisa mengakses view, bukan tabel dasarnya. MySQL memisahkan hak akses per objek, sehingga kita bisa membuat akun khusus yang hanya boleh membaca satu view. Pembuatan user dan pemberian hak kita lakukan dari sesi root:
DROP USER IF EXISTS 'staf_hrd'@'localhost';
CREATE USER IF NOT EXISTS 'staf_hrd'@'localhost' IDENTIFIED BY 'SandiLatihan123';
GRANT SELECT ON belajar_view.v_karyawan_publik TO 'staf_hrd'@'localhost';Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Mari kita bedah tiga statement di atas: DROP USER IF EXISTS membersihkan akun bernama sama dari percobaan sebelumnya supaya rangkaian ini aman diulang; CREATE USER IF NOT EXISTS membuat akun staf_hrd yang hanya bisa login dari mesin yang sama (localhost) dengan kata sandi latihan; GRANT SELECT ON belajar_view.v_karyawan_publik memberikan hak baca pada tepat satu objek — view publik tanpa gaji. Hak yang benar-benar melekat pada akun ini bisa kita periksa dengan SHOW GRANTS:
SHOW GRANTS FOR 'staf_hrd'@'localhost';Output yang Dihasilkan:
+------------------------------------------------------------------------------+
| Grants for staf_hrd@localhost |
+------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `staf_hrd`@`localhost` |
| GRANT SELECT ON `belajar_view`.`v_karyawan_publik` TO `staf_hrd`@`localhost` |
+------------------------------------------------------------------------------+
2 rows in set (0.00 sec)Dua baris hasil merangkum seluruh kekuasaan akun ini: GRANT USAGE ON *.* berarti tanpa hak global apa pun, dan GRANT SELECT ON belajar_view.v_karyawan_publik adalah satu-satunya hak objek yang dimiliki. Untuk mencoba sendiri dari terminal kedua, pembaca bisa membuka koneksi baru sebagai user tersebut:
mysql -h 127.0.0.1 -P 3306 -u staf_hrd -p belajar_viewDari sesi itu, SELECT * FROM v_karyawan_publik berjalan normal dan mengembalikan delapan baris data pegawai tanpa kolom gaji. Sebaliknya SELECT * FROM karyawan ditolak dengan ERROR 1142 (42000): SELECT command denied to user 'staf_hrd'@'localhost' for table 'karyawan' — tabel dasar memang tidak pernah diberikan kepadanya. Statement tulis seperti INSERT maupun UPDATE pada view juga ditolak dengan error 1142 karena hak yang diberikan hanya SELECT. Ketika di kemudian hari hak baca ini perlu dicabut tanpa membuang akunnya, statement REVOKE SELECT ON belajar_view.v_karyawan_publik FROM 'staf_hrd'@'localhost' menanganinya dari sesi root.
Membersihkan Database Latihan #
Kita tutup latihan dengan menjatuhkan user uji beserta database latihannya — dua statement yang sama-sama aman diulang berkat IF EXISTS:
DROP USER IF EXISTS 'staf_hrd'@'localhost';
DROP DATABASE belajar_view;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 4 rows affected (0.00 sec)Referensi #
- CREATE VIEW Statement — MySQL 8.4 Reference Manual
- GRANT Statement — MySQL 8.4 Reference Manual
- Privileges Provided by MySQL — MySQL 8.4 Reference Manual
View kini lengkap sebagai instrumen keamanan: kolom sensitif tersembunyi, baris terjaga oleh CHECK OPTION, dan akses user dibatasi per objek dengan GRANT. Satu pertanyaan besar masih menggantung — view mana yang boleh menerima INSERT, UPDATE, dan DELETE seperti tabel sungguhan, dan view mana yang ditolak keras dengan ERROR 1288? Chapter berikutnya membedah aturan updatability beserta seluruh batasannya.