Di mata pembaca, view tampil persis seperti tabel — padahal di baliknya hanya ada query tersimpan. Kesamaan tampilan ini memunculkan pertanyaan penting: bolehkah kita menjalankan INSERT, UPDATE, atau DELETE terhadap view? Jawaban MySQL bernada “ya, dengan syarat”: hanya view yang strukturnya cukup sederhana yang bisa menjadi perantara perubahan data, dan sebagian kolomnya pun bisa saja tetap terkunci. Mengirim statement tulis ke view yang salah bentuk bukan kejadian yang jarang — respons servernya langsung dan keras, misalnya ERROR 1288 untuk view beragregat. Pada chapter penutup section ini kita petakan seluruh batasan updatability dengan uji nyata di tiap kasus, lalu menutupnya dengan pemeriksaan CHECK OPTION bertingkat.
Menyiapkan Tabel Latihan #
Batasan view paling jelas terlihat ketika tabel dasarnya berelasi, jadi kali ini kita menyiapkan dua tabel: karyawan delapan baris dan departemen empat baris yang dihubungkan kolom departemen:
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');
CREATE TABLE departemen (
nama VARCHAR(30) PRIMARY KEY,
lokasi VARCHAR(50) NOT NULL
);
INSERT INTO departemen (nama, lokasi) VALUES
('IT', 'Lantai 3'),
('Keuangan', 'Lantai 1'),
('HR', 'Lantai 2'),
('Pemasaran', 'Lantai 4');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0
Query OK, 0 rows affected (0.00 sec)
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0Tabel departemen memetakan setiap nama departemen ke lokasi kantor, sehingga view ber-JOIN di bawah punya pasangan tabel yang bermakna. Seluruh angka gaji sengaja berbeda-beda supaya dampak setiap UPDATE mudah dilacak pada tabel dasar.
View Sederhana Bisa Diubah #
Titik awal paling menggembirakan: view yang definisinya hanya membaca satu tabel, memilih kolom langsung tanpa perhitungan, tanpa agregat, dan tanpa pengelompokan — boleh menerima statement tulis. Kita buat view v_karyawan semodel itu, lalu mengubah gaji Siti lewat view:
CREATE VIEW v_karyawan AS
SELECT id, nama, gaji, departemen, tanggal_masuk FROM karyawan;
UPDATE v_karyawan SET gaji = 7350000.00 WHERE id = 2;
SELECT id, nama, gaji FROM karyawan WHERE id = 2;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+----+-------------+------------+
| id | nama | gaji |
+----+-------------+------------+
| 2 | Siti Rahayu | 7350000.00 |
+----+-------------+------------+
1 row in set (0.00 sec)UPDATE lewat view sukses (Query OK, 1 row affected), dan pembacaan langsung ke tabel karyawan memastikan gaji Siti berubah menjadi 7350000.00. Mekanismenya: MySQL menerjemahkan statement UPDATE v_karyawan SET gaji = ... WHERE id = 2 menjadi operasi pada tabel dasar — kolom view gaji dipetakan ke kolom karyawan.gaji, kondisi id = 2 diteruskan apa adanya, lalu tabel karyawan yang benar-benar diubah. Karena pemetaan setiap kolom bersifat satu-ke-satu dengan kolom asli, tidak ada ambiguitas yang harus diselesaikan server. Syarat yang sama membuat INSERT dan DELETE juga sah melalui view jenis ini — pola yang sudah kita manfaatkan pada chapter keamanan data dengan WITH CHECK OPTION.
View Beragregat Tidak Bisa Diubah #
Situasi berbalik total ketika definisi view memuat fungsi agregat dan GROUP BY. Kita buat view statistik per departemen, lalu mencoba memanipulasi salah satu angkanya:
CREATE VIEW v_statistik_departemen AS
SELECT departemen,
COUNT(*) AS jumlah_karyawan,
SUM(gaji) AS total_gaji,
MAX(gaji) AS gaji_tertinggi
FROM karyawan
GROUP BY departemen;
SELECT * FROM v_statistik_departemen ORDER BY departemen;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+------------+-----------------+-------------+----------------+
| departemen | jumlah_karyawan | total_gaji | gaji_tertinggi |
+------------+-----------------+-------------+----------------+
| HR | 2 | 11800000.00 | 6300000.00 |
| IT | 3 | 23550000.00 | 8500000.00 |
| Keuangan | 2 | 16300000.00 | 9500000.00 |
| Pemasaran | 1 | 8900000.00 | 8900000.00 |
+------------+-----------------+-------------+----------------+
4 rows in set (0.00 sec)-- @error
UPDATE v_statistik_departemen SET total_gaji = 0 WHERE departemen = 'IT';Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1288 (HY000): The target table v_statistik_departemen of the UPDATE is not updatable
ByeServer menolak dengan ERROR 1288 (HY000): The target table v_statistik_departemen of the UPDATE is not updatable. Penolakan ini masuk akal begitu kita bedah struktur kedua sisi: satu baris view — misalnya baris IT dengan jumlah_karyawan 3 dan total_gaji 23550000.00 — adalah gabungan tiga baris tabel karyawan. Server tidak punya jawaban untuk pertanyaan “baris karyawan mana yang harus diubah bila total_gaji mau dijadikan nol?”, dan kolom agregat seperti total_gaji sama sekali tidak punya kolom asal untuk ditulis. Larangan ini berlaku merata: INSERT ke view beragregat ditolak dengan ERROR 1471 (The target table ... is not insertable-into), DELETE kembali ditolak ERROR 1288. Selain agregat dan GROUP BY, dokumentasi resmi mencantumkan kondisi-kondisi lain yang membuat view tidak updatable — antara lain DISTINCT, HAVING, LIMIT, dan UNION — semuanya karena alasan serupa: baris view tidak lagi berpasangan satu-satu dengan baris tabel dasar.
View dengan JOIN: Hanya Kolom Satu Tabel #
View yang menggabungkan dua tabel menempati zona tengah: secara utuh tidak bisa menerima baris baru, tetapi kolom-kolomnya tetap bisa diperbarui sepanjang perubahan hanya menyentuh satu tabel dasar. Kita buat view detail yang menggabungkan karyawan dan departemen:
CREATE VIEW v_detail_karyawan AS
SELECT k.id, k.nama, k.gaji, k.departemen, d.lokasi
FROM karyawan k JOIN departemen d ON k.departemen = d.nama;
SELECT id, nama, departemen, lokasi FROM v_detail_karyawan ORDER BY id;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+----+----------------+------------+----------+
| id | nama | departemen | lokasi |
+----+----------------+------------+----------+
| 1 | Budi Santoso | IT | Lantai 3 |
| 2 | Siti Rahayu | IT | Lantai 3 |
| 3 | Andi Wijaya | Keuangan | Lantai 1 |
| 4 | Dewi Lestari | Keuangan | Lantai 1 |
| 5 | Rina Kurnia | HR | Lantai 2 |
| 6 | Tono Prasetyo | IT | Lantai 3 |
| 7 | Sari Wulandari | HR | Lantai 2 |
| 8 | Joko Susilo | Pemasaran | Lantai 4 |
+----+----------------+------------+----------+
8 rows in set (0.00 sec)Kolom gaji pada view ini berasal dari tabel karyawan, sementara kolom lokasi berasal dari tabel departemen. UPDATE yang hanya menyentuh kolom dari satu tabel diterjemahkan tanpa masalah:
UPDATE v_detail_karyawan SET gaji = 9700000.00 WHERE id = 3;
SELECT id, nama, gaji FROM karyawan WHERE id = 3;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+----+-------------+------------+
| id | nama | gaji |
+----+-------------+------------+
| 3 | Andi Wijaya | 9700000.00 |
+----+-------------+------------+
1 row in set (0.00 sec)Gaji Andi di tabel karyawan berubah menjadi 9700000.00 — hanya tabel karyawan yang tersentuh. Perubahan arah sebaliknya juga sah, selama kolomnya masih utuh dari satu tabel:
UPDATE v_detail_karyawan SET lokasi = 'Lantai 5' WHERE departemen = 'IT';
SELECT nama, lokasi FROM departemen ORDER BY nama;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+-----------+----------+
| nama | lokasi |
+-----------+----------+
| HR | Lantai 2 |
| IT | Lantai 5 |
| Keuangan | Lantai 1 |
| Pemasaran | Lantai 4 |
+-----------+----------+
4 rows in set (0.00 sec)Lokasi seluruh pegawai IT kini terbaca Lantai 5 karena UPDATE diterjemahkan menjadi perubahan pada satu baris tabel departemen. Sebaliknya, statement yang mencoba mengisi dua tabel dasar sekaligus ditolak keras:
-- @error
INSERT INTO v_detail_karyawan (id, nama, gaji, departemen, lokasi) VALUES (99, 'Uji Insert', 1000000.00, 'IT', 'Lantai 9');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1393 (HY000): Can not modify more than one base table through a join view 'belajar_view.v_detail_karyawan'
ByeERROR 1393 (HY000): Can not modify more than one base table through a join view 'belajar_view.v_detail_karyawan' — daftar kolom penyisipan menyinggung karyawan sekaligus departemen, dan MySQL tidak mengizinkan satu statement view menulis ke dua tabel dasar. Statement DELETE FROM v_detail_karyawan ditolak dengan alasan serupa lewat ERROR 1395 (Can not delete from join view ...) karena penghapusan satu baris view berpotensi menghapus baris di dua tabel.
Kolom Terhitung Tidak Bisa Diubah #
Batasan berikutnya hidup di tingkat kolom, bukan view. View boleh memuat kolom terhitung sekaligus kolom langsung — dan hanya kolom terhitungnya yang menolak perubahan:
CREATE VIEW v_gaji_tahunan AS
SELECT id, nama, gaji, gaji * 12 AS gaji_tahunan FROM karyawan;
SELECT * FROM v_gaji_tahunan WHERE id = 1;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+----+--------------+------------+--------------+
| id | nama | gaji | gaji_tahunan |
+----+--------------+------------+--------------+
| 1 | Budi Santoso | 8500000.00 | 102000000.00 |
+----+--------------+------------+--------------+
1 row in set (0.00 sec)Kolom gaji_tahunan menampilkan 102000000.00 — hasil 8500000.00 * 12 yang dihitung saat view dibaca. Mencoba menulis ke kolom terhitung tersebut gagal:
-- @error
UPDATE v_gaji_tahunan SET gaji_tahunan = 120000000.00 WHERE id = 1;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1348 (HY000): Column 'gaji_tahunan' is not updatable
ByeERROR 1348 (HY000): Column 'gaji_tahunan' is not updatable muncul karena kolom ini tidak punya kolom asal di karyawan — nilai-nilainya lahir dari ekspresi gaji * 12, bukan dari penyimpanan. Padahal kolom gaji di view yang sama tetap normal di-update, dan kolom terhitungnya otomatis menghitung ulang:
UPDATE v_gaji_tahunan SET gaji = 8600000.00 WHERE id = 1;
SELECT * FROM v_gaji_tahunan WHERE id = 1;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+----+--------------+------------+--------------+
| id | nama | gaji | gaji_tahunan |
+----+--------------+------------+--------------+
| 1 | Budi Santoso | 8600000.00 | 103200000.00 |
+----+--------------+------------+--------------+
1 row in set (0.00 sec)Gaji Budi berubah menjadi 8600000.00 dan gaji_tahunan mengikutinya menjadi 103200000.00 — dibaca ulang dari tabel dasar yang baru diubah, bukan disimpan di view.
CHECK OPTION Bertingkat: LOCAL dan CASCADED #
Bagian penutup kembali ke WITH CHECK OPTION, kini pada view yang ditumpuk di atas view lain. Pilihan kata kuncinya dua: CASCADED (bawaan bila hanya menulis WITH CHECK OPTION) memeriksa klausa WHERE view itu sendiri plus seluruh view di bawahnya; LOCAL hanya memeriksa WHERE view yang sedang diubah, melewatkan view bawah yang tidak punya pengaman sendiri. Kita bangun tiga lapisan uji:
CREATE VIEW v_staf_it AS
SELECT id, nama, gaji, departemen, tanggal_masuk FROM karyawan WHERE departemen = 'IT';
CREATE VIEW v_it_gaji_casc AS
SELECT * FROM v_staf_it WHERE gaji <= 9000000 WITH CASCADED CHECK OPTION;
CREATE VIEW v_it_gaji_lokal AS
SELECT * FROM v_staf_it WHERE gaji <= 9000000 WITH LOCAL CHECK OPTION;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Induk v_staf_it menyaring departemen = 'IT' tanpa check option; kedua anaknya menyaring gaji <= 9000000 dengan jenis pemeriksaan berbeda. Kita kirim baris yang lolos syarat anak (gaji 5000000) tetapi melanggar syarat induk (departemen HR) lewat masing-masing anak:
-- @error
INSERT INTO v_it_gaji_casc (nama, gaji, departemen, tanggal_masuk) VALUES ('Coba Cascade', 5000000.00, 'HR', '2026-03-01');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1369 (HY000): CHECK OPTION failed 'belajar_view.v_it_gaji_casc'
ByeINSERT INTO v_it_gaji_lokal (nama, gaji, departemen, tanggal_masuk)
VALUES ('Coba Lokal', 5000000.00, 'HR', '2026-03-01');
SELECT id, nama, departemen FROM karyawan WHERE departemen = 'HR' ORDER BY id;
SELECT COUNT(*) AS baris_di_view_induk FROM v_staf_it;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
+----+----------------+------------+
| id | nama | departemen |
+----+----------------+------------+
| 5 | Rina Kurnia | HR |
| 7 | Sari Wulandari | HR |
| 9 | Coba Lokal | HR |
+----+----------------+------------+
3 rows in set (0.00 sec)
+---------------------+
| baris_di_view_induk |
+---------------------+
| 3 |
+---------------------+
1 row in set (0.00 sec)Hasilnya kontras sempurna: penyisipan lewat v_it_gaji_casc ditolak ERROR 1369 (HY000): CHECK OPTION failed 'belajar_view.v_it_gaji_casc' karena mode CASCADED ikut memeriksa WHERE departemen = 'IT' milik induk — padahal induknya sendiri tidak pernah menyatakan check option. Penyisipan yang sama lewat v_it_gaji_lokal justru lolos: LOCAL berhenti pada pemeriksaan gaji <= 9000000 milik view yang diubah. Baris Coba Lokal pun tersimpan di tabel karyawan (terlihat pada daftar departemen HR yang kini tiga baris) namun tidak pernah muncul di v_staf_it maupun kedua anaknya — baris hantu dari view bertingkat. Satu catatan penting: LOCAL tetap memeriksa view bawah yang memiliki WITH CHECK OPTION sendiri; yang dilewatkan hanya klausa WHERE view bawah yang polos. Baris uji kita bereskan:
DELETE FROM karyawan WHERE nama = 'Coba Lokal';Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Ringkasan Batasan Updatability #
Kita tutup dengan membaca laporan metadata seluruh view chapter ini, lalu merangkum aturan mainnya:
SELECT TABLE_NAME, IS_UPDATABLE FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = 'belajar_view' ORDER BY TABLE_NAME;Output yang Dihasilkan:
+------------------------+--------------+
| TABLE_NAME | IS_UPDATABLE |
+------------------------+--------------+
| v_detail_karyawan | YES |
| v_gaji_tahunan | YES |
| v_it_gaji_casc | YES |
| v_it_gaji_lokal | YES |
| v_karyawan | YES |
| v_staf_it | YES |
| v_statistik_departemen | NO |
+------------------------+--------------+
7 rows in set (0.00 sec)| Kondisi pada definisi view | Akibat terhadap DML |
|---|---|
| Satu tabel, kolom langsung, tanpa agregat | INSERT, UPDATE, DELETE diterjemahkan ke tabel dasar |
Agregat, GROUP BY, DISTINCT, HAVING, LIMIT, UNION | Semua DML ditolak (ERROR 1288 / 1471) |
JOIN beberapa tabel | UPDATE sah bila hanya menyentuh kolom satu tabel; INSERT ditolak ERROR 1393, DELETE ditolak ERROR 1395 |
| Kolom terhitung pada view updatable | Kolom terhitung menolak tulis (ERROR 1348), kolom asli tetap boleh |
Membersihkan Database Latihan #
Peta batasan lengkap; database latihan kita jatuhkan:
DROP DATABASE belajar_view;Output yang Dihasilkan:
Query OK, 9 rows affected (0.00 sec)Referensi #
- View Processing — MySQL 8.4 Reference Manual
- View Algorithms — MySQL 8.4 Reference Manual
- CREATE VIEW Statement — MySQL 8.4 Reference Manual
Kita kini memegang peta lengkap view: cara membuatnya, mengubahnya, menghapusnya, memakainya sebagai lapisan keamanan, dan batas-batas ketika view dipakai untuk menulis data. Pola yang terus berulang sepanjang section ini adalah satu transaksi per statement — satu UPDATE lewat view mengubah satu tabel. Kenyataan aplikasi sering menuntut lebih: beberapa tabel harus berubah bersamaan atau tidak sama sekali. Kebutuhan itulah yang dijawab section berikutnya: transaction dan COMMIT-ROLLBACK.