Loop di chapter sebelumnya bekerja dengan angka buatan sendiri. Tugas nyata jauh lebih sering berbunyi “proses setiap baris hasil query” — dan SELECT ... INTO hanya sanggup menampung satu baris. Cursor mengisi kekosongan ini: semacam penunjuk yang berjalan di atas hasil query di dalam stored procedure, maju satu baris setiap kali perintah FETCH dijalankan, dan berhenti ketika data habis. Pada chapter ini kita membangun cursor pertama lengkap dengan handler NOT FOUND sebagai rem pengereman, membedah polanya baris per baris, membuktikan urutan deklarasi yang diwajibkan MySQL, lalu menutup dengan pola nested BEGIN ... END untuk membatasi jangkauan handler.
Menyiapkan Database Latihan #
Database belajar_sp dan tabel produk dibangun ulang seperti biasa:
DROP DATABASE IF EXISTS belajar_sp;
CREATE DATABASE belajar_sp;
USE belajar_sp;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE produk (
id INT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
harga DECIMAL(10,2) NOT NULL,
stok INT NOT NULL
);
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo', 55000.00, 10),
(2, 'Teh Melati', 25000.00, 20),
(3, 'Gula Aren', 15000.00, 30),
(4, 'Susu Kental', 8000.00, 40),
(5, 'Kopi Toraja', 62000.00, 5);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0Empat Langkah Hidup Sebuah Cursor #
Cursor dikenal lewat empat statement yang selalu muncul berurutan:
| Statement | Peran |
|---|---|
DECLARE cur CURSOR FOR SELECT ... | mendefinisikan query yang akan ditelusuri |
OPEN cur | menjalankan query dan menempatkan penunjuk sebelum baris pertama |
FETCH cur INTO v_a, v_b | mengambil baris berikutnya ke dalam variabel |
CLOSE cur | melepas hasil query dan memorinya |
Satu cursor tidak bisa “melompat” atau mundur — hanya maju, satu baris per FETCH. Karena itu pola baku cursor selalu berbentuk loop FETCH yang berputar sampai data habis, dan deteksi “habis” itulah tugas handler NOT FOUND.
Cursor Pertama: Membaca Setiap Baris Produk #
Prosedur baca_produk menelusuri seluruh produk lalu mencetak id dan nama setiap baris sebagai result set terpisah:
DROP PROCEDURE IF EXISTS baca_produk;
DELIMITER $$
CREATE PROCEDURE baca_produk()
BEGIN
DECLARE v_selesai TINYINT DEFAULT 0;
DECLARE v_id INT;
DECLARE v_nama VARCHAR(50);
DECLARE cur CURSOR FOR SELECT id, nama FROM produk ORDER BY id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_selesai = 1;
OPEN cur;
baca: LOOP
FETCH cur INTO v_id, v_nama;
IF v_selesai = 1 THEN
LEAVE baca;
END IF;
SELECT v_id AS id, v_nama AS nama;
END LOOP baca;
CLOSE cur;
END$$
DELIMITER ;
CALL baca_produk();Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
+------+-----------+
| id | nama |
+------+-----------+
| 1 | Kopi Gayo |
+------+-----------+
1 row in set (0.00 sec)
+------+------------+
| id | nama |
+------+------------+
| 2 | Teh Melati |
+------+------------+
1 row in set (0.00 sec)
+------+-----------+
| id | nama |
+------+-----------+
| 3 | Gula Aren |
+------+-----------+
1 row in set (0.00 sec)
+------+-------------+
| id | nama |
+------+-------------+
| 4 | Susu Kental |
+------+-------------+
1 row in set (0.00 sec)
+------+-------------+
| id | nama |
+------+-------------+
| 5 | Kopi Toraja |
+------+-------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Lima baris produk tercetak sebagai lima result set kecil, terurut dari Kopi Gayo sampai Kopi Toraja. Pola “flag + handler” pada prosedur ini adalah jantung setiap cursor, jadi mari kita bedah baris per baris:
DECLARE v_selesai TINYINT DEFAULT 0;menyiapkan penanda berhenti. Nilai awal0berarti “masih ada data”; nilai1nanti di-set oleh handler sebagai tanda “data habis”.DECLARE v_id INT;danDECLARE v_nama VARCHAR(50);menyiapkan penampung baris. Tipe variabel sebaiknya cocok dengan kolom yang diambil —INTuntukid,VARCHAR(50)untuknama.DECLARE cur CURSOR FOR SELECT id, nama FROM produk ORDER BY id;mendefinisikan cursor bernamacurbeserta query-nya. Query belum dijalankan di titik ini — baru dideklarasikan.ORDER BY idkita tulis eksplisit agar urutan baris yang diterimaFETCHdeterministik: tanpaORDER BY, server bebas mengembalikan baris dalam urutan apa pun.DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_selesai = 1;memasang penangkap untuk kondisiNOT FOUND. KetikaFETCHberjalan melewati baris terakhir, server menaikkan kondisi SQLSTATE02000— kelas “no data” — dan handler ini menangkapnya, men-setv_selesai = 1, lalu jenisCONTINUEmembuat eksekusi melanjutkan statement setelahFETCHyang gagal tersebut.OPEN cur;menjalankan query dan menempatkan penunjuk sebelum baris pertama. Hasil query disiapkan server — pada tabel kecil di memori, pada tabel besar bisa berupa streaming dari storage engine.baca: LOOPmemberi label pada loop. Label dibutuhkan karenaLEAVEhanya bisa menunjuk loop yang bernama.FETCH cur INTO v_id, v_nama;memajukan penunjuk satu baris dan menyalin nilai kolom ke variabel. Ketika baris sudah habis,FETCHinilah yang memicuNOT FOUND— bukanOPEN, bukanCLOSE.IF v_selesai = 1 THEN LEAVE baca;memeriksa penanda setiap putaran. UrutanFETCH→ cek flag → proses baris membuat iterasi terakhir (yang gagalFETCH) keluar dengan rapi sebelum sempat memproses nilai basi.SELECT v_id AS id, v_nama AS nama;memproses baris yang baru diambil — di prosedur nyata bagian ini bisa berupa perhitungan,INSERTke tabel log, atau akumulasi.CLOSE cur;melepas hasil query. Cursor yang lupa ditutup tetap dibebaskan saat prosedur berakhir, tetapi menutup secara eksplisit adalah kebiasaan yang menjaga kebersihan resource.
Handler NOT FOUND Wajib Dipasang Sebelum Loop #
Bagian paling rawan salah pemahaman adalah interaksi FETCH dengan handler. Tanpa DECLARE ... HANDLER FOR NOT FOUND, FETCH yang kehabisan data memunculkan error dan menghentikan prosedur begitu saja — loop tidak pernah tahu kapan harus berhenti. Dengan handler CONTINUE, kegagalan FETCH tertangkap dan diubah menjadi sinyal yang bisa kita periksa: flag v_selesai berubah menjadi 1, eksekusi berlanjut ke statement berikutnya (yaitu IF v_selesai = 1), dan loop keluar melalui LEAVE.
NOT FOUND sendiri adalah nama ringkas untuk kelas SQLSTATE 02000 — kode standar SQL untuk “operasi tidak menemukan data”. Handler bisa juga ditulis eksplisit DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' ... dengan makna yang sama. Pemanggilan berikutnya menunjukkan betapa tenangnya prosedur ketika tabelnya kosong:
DELETE FROM produk;
CALL baca_produk();Output yang Dihasilkan:
Query OK, 5 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Seluruh baris produk kita hapus lebih dulu, sehingga FETCH langsung kehabisan data pada putaran pertama. Handler men-set flag, loop keluar tanpa satu baris pun tercetak, dan CALL berakhir dengan sukses — inilah perilaku yang kita inginkan dari loop data yang terpenuhi maupun tidak.
Urutan Deklarasi: Variabel, Cursor, lalu Handler #
MySQL mewajibkan urutan ketat di awal blok: variabel dan kondisi dideklarasikan lebih dulu, disusul cursor, dan paling akhir handler. Menukar urutan cursor dengan handler langsung ditolak:
-- @error
DELIMITER $$
CREATE PROCEDURE urutan_salah() BEGIN DECLARE v_selesai TINYINT DEFAULT 0; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_selesai = 1; DECLARE cur CURSOR FOR SELECT id FROM produk; END$$
DELIMITER ;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1338 (42000): Cursor declaration after handler declaration
ByeERROR 1338 (42000): Cursor declaration after handler declaration menyebut pelanggarannya dengan jelas: cursor cur dideklarasikan setelah handler. Aturan ini bukan formalitas — MySQL memproses deklarasi secara berurutan, dan handler harus sudah tahu seluruh cursor yang ada di bloknya agar pemasangan kondisi NOT FOUND bekerja benar. Skema aman yang selalu berhasil: blok deklarasi dibaca seperti daftar isi — variabel, cursor, handler, baru statement kerja.
Akumulasi Nilai per Baris #
Mencetak baris satu per satu memang jelas terlihat, namun pemanfaatan cursor yang paling umum adalah akumulasi: menjumlah, menghitung, atau mengagregasi nilai sambil menelusuri. Prosedur nilai_persediaan menghitung harga * stok setiap produk dan mengembalikan totalnya lewat parameter OUT. Tabel latihan kita isi kembali lebih dulu karena barisnya barusan dihapus:
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo', 55000.00, 10),
(2, 'Teh Melati', 25000.00, 20),
(3, 'Gula Aren', 15000.00, 30),
(4, 'Susu Kental', 8000.00, 40),
(5, 'Kopi Toraja', 62000.00, 5);Output yang Dihasilkan:
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0DROP PROCEDURE IF EXISTS nilai_persediaan;
DELIMITER $$
CREATE PROCEDURE nilai_persediaan(OUT p_total DECIMAL(12,2))
BEGIN
DECLARE v_selesai TINYINT DEFAULT 0;
DECLARE v_harga DECIMAL(10,2);
DECLARE v_stok INT;
DECLARE cur CURSOR FOR SELECT harga, stok FROM produk ORDER BY id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_selesai = 1;
SET p_total = 0;
OPEN cur;
ulang: LOOP
FETCH cur INTO v_harga, v_stok;
IF v_selesai = 1 THEN
LEAVE ulang;
END IF;
SET p_total = p_total + v_harga * v_stok;
END LOOP ulang;
CLOSE cur;
END$$
DELIMITER ;
CALL nilai_persediaan(@totalnya);
SELECT @totalnya AS nilai_persediaan;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)
+------------------+
| nilai_persediaan |
+------------------+
| 2130000.00 |
+------------------+
1 row in set (0.00 sec)Total 2130000.00 terkumpul dari lima baris — hasil yang sama dengan SELECT SUM(harga * stok) FROM produk. Perbedaan pentingnya: versi cursor memproses setiap baris secara individual, sehingga bisa diselipkan logika per baris (diskon khusus, pengecualian produk tertentu, pencatatan audit) yang sulit dinyatakan dalam satu agregasi. Akumulator p_total kembali kita nolkan sebelum loop karena parameter OUT berangkat dari NULL dan NULL + apa pun tetap NULL.
Nested BEGIN: Mempersempit Jangkauan Handler #
Handler berlaku untuk seluruh blok tempat ia dideklarasikan — termasuk semua statement di dalamnya. Ketika prosedur juga menjalankan SELECT biasa di luar proses cursor, handler NOT FOUND yang sama ikut menangkap kondisi tak terduga dari statement tersebut. Solusi idiomatiknya: bungkus proses cursor dalam blok BEGIN ... END bersarang dan pasang handler di blok dalam, sehingga jangkauannya berhenti di kurung penutupnya:
DROP PROCEDURE IF EXISTS laporan_cursor;
DELIMITER $$
CREATE PROCEDURE laporan_cursor()
BEGIN
DECLARE v_selesai TINYINT DEFAULT 0;
DECLARE v_nama VARCHAR(50);
DECLARE cur CURSOR FOR SELECT nama FROM produk ORDER BY id;
BEGIN
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_selesai = 1;
OPEN cur;
ulang: LOOP
FETCH cur INTO v_nama;
IF v_selesai = 1 THEN LEAVE ulang; END IF;
SELECT v_nama AS nama_produk;
END LOOP ulang;
CLOSE cur;
END;
SELECT 'laporan selesai' AS status;
END$$
DELIMITER ;
CALL laporan_cursor();Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
+-------------+
| nama_produk |
+-------------+
| Kopi Gayo |
+-------------+
1 row in set (0.00 sec)
+-------------+
| nama_produk |
+-------------+
| Teh Melati |
+-------------+
1 row in set (0.00 sec)
+-------------+
| nama_produk |
+-------------+
| Gula Aren |
+-------------+
1 row in set (0.00 sec)
+-------------+
| nama_produk |
+-------------+
| Susu Kental |
+-------------+
1 row in set (0.00 sec)
+-------------+
| nama_produk |
+-------------+
| Kopi Toraja |
+-------------+
1 row in set (0.00 sec)
+-----------------+
| status |
+-----------------+
| laporan selesai |
+-----------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Lima nama produk tercetak dari blok dalam, lalu baris laporan selesai menyusul dari blok luar — urutan yang membuktikan loop tuntas lebih dulu dan eksekusi mengalir keluar blok dengan normal. Struktur seperti ini juga berguna untuk scope variabel: v_nama dan v_selesai dideklarasikan di blok luar sehingga masih bisa dibaca setelah blok cursor selesai, sementara handler yang memodifikasinya terkunci rapi di dalam. Blok bersarang boleh berlapis lebih dari satu, dan setiap lapisan berhak memiliki deklarasi variabel serta handler-nya sendiri.
Membersihkan Database Latihan #
DROP DATABASE belajar_sp;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- Cursors — MySQL 8.4 Reference Manual
- DECLARE … HANDLER Statement — MySQL 8.4 Reference Manual
- Compound Statement Syntax — MySQL 8.4 Reference Manual
Cursor melengkapi kotak peralatan prosedur kita: query dideklarasikan sebagai cursor, ditelusuri baris demi baris dengan FETCH, dihentikan dengan sopan oleh handler NOT FOUND, dan ditutup dengan CLOSE. Loop kita kini bisa memproses data nyata dengan urutan yang terjamin lewat ORDER BY. Namun satu kelemahan besar masih tersisa: kegagalan SQL di tengah prosedur — duplikat kunci, data kosong, pelanggaran constraint — langsung menghentikan semuanya tanpa kesempatan menjelaskan apa yang terjadi. Chapter berikutnya membedah handler secara penuh: menangkap error, membaca pesannya lewat GET DIAGNOSTICS, bahkan melempar error kustom kita sendiri dengan SIGNAL.