Prosedur yang kita buat di chapter sebelumnya selalu menghasilkan hal yang sama karena tidak menerima masukan apa pun. Padahal kekuatan prosedur justru muncul ketika pemanggil bisa menentukan sendiri nilai yang diproses: batas harga, jumlah stok, atau persen diskon. Parameter mengisi kekosongan itu — variabel resmi di kepala prosedur yang diisi saat CALL dijalankan. MySQL mengenal tiga mode: IN untuk masukan, OUT untuk keluaran, dan INOUT untuk keduanya. Pada chapter ini kita membedah ketiganya satu per satu, membuat prosedur dengan beberapa parameter, menambahkan validasi input, lalu membuktikan bahwa jumlah argumen saat CALL tidak boleh kurang maupun lebih.

Menyiapkan Database Latihan #

Kita pakai kembali database latihan belajar_sp dengan tabel produk yang sama seperti chapter sebelumnya supaya angka-angkanya mudah dibandingkan:

sql
DROP DATABASE IF EXISTS belajar_sp;
CREATE DATABASE belajar_sp;
USE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE 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:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

Tiga Mode Parameter #

Parameter dideklarasikan di dalam kurung nama prosedur dengan pola mode nama tipe. Tabel berikut merangkum perilaku ketiganya:

ModeArahNilai awal saat prosedur jalanDipakai untuk
IN (bawaan)pemanggil → prosedurnilai dari argumen CALLmasukan
OUTprosedur → pemanggilNULLkeluaran
INOUTdua arahnilai argumen CALLmasukan sekaligus keluaran

Jika mode tidak ditulis, MySQL memperlakukannya sebagai IN. Nilai yang keluar lewat OUT dan INOUT ditampung oleh variabel sesi (@nama) yang kita siapkan di sisi pemanggil — variabel inilah yang dibaca setelah CALL selesai.

Parameter IN: Masukan dari Pemanggil #

Prosedur produk_mahal menerima batas harga, lalu mengembalikan produk yang harganya minimal setara batas tersebut. Kita tulis mode IN secara eksplisit agar niatnya terbaca:

sql
DROP PROCEDURE IF EXISTS produk_mahal;
DELIMITER $$
CREATE PROCEDURE produk_mahal(IN p_batas DECIMAL(10,2))
BEGIN
  SELECT id, nama, harga FROM produk
  WHERE harga >= p_batas
  ORDER BY harga DESC;
END$$
DELIMITER ;
CALL produk_mahal(50000.00);
CALL produk_mahal(20000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----+-------------+----------+
| id | nama        | harga    |
+----+-------------+----------+
|  5 | Kopi Toraja | 62000.00 |
|  1 | Kopi Gayo   | 55000.00 |
+----+-------------+----------+
2 rows in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----+-------------+----------+
| id | nama        | harga    |
+----+-------------+----------+
|  5 | Kopi Toraja | 62000.00 |
|  1 | Kopi Gayo   | 55000.00 |
|  2 | Teh Melati  | 25000.00 |
+----+-------------+----------+
3 rows in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Prosedur yang sama menghasilkan dua laporan berbeda hanya karena argumennya diganti — persis perilaku fungsi pada bahasa pemrograman umum. Sekarang kita buktikan bahwa IN memang mode bawaan dengan menghilangkan kata IN:

sql
DROP PROCEDURE IF EXISTS produk_murah;
DELIMITER $$
CREATE PROCEDURE produk_murah(p_batas DECIMAL(10,2))
BEGIN
  SELECT id, nama, harga FROM produk
  WHERE harga <= p_batas
  ORDER BY harga;
END$$
DELIMITER ;
CALL produk_murah(10000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----+-------------+---------+
| id | nama        | harga   |
+----+-------------+---------+
|  4 | Susu Kental | 8000.00 |
+----+-------------+---------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Prosedur tanpa kata IN diterima dan berperilaku sama persis. Dalam kode nyata, menuliskan mode secara eksplisit tetap lebih baik: pembaca langsung tahu arah data tanpa menebak.

Parameter OUT: Keluaran via Variabel Sesi #

Parameter OUT membawa nilai keluar dari prosedur. Pemanggil harus menyediakan variabel sesi sebagai penampung — di klien mysql variabel semacam itu ditulis dengan awalan @. Prosedur total_stok menghitung seluruh stok lalu mengeluarkannya:

sql
DROP PROCEDURE IF EXISTS total_stok;
DELIMITER $$
CREATE PROCEDURE total_stok(OUT p_total INT)
BEGIN
  SELECT SUM(stok) INTO p_total FROM produk;
END$$
DELIMITER ;
CALL total_stok(@hasil);
SELECT @hasil AS total_stok;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

+------------+
| total_stok |
+------------+
|        105 |
+------------+
1 row in set (0.00 sec)

Mari kita bedah alur datanya: CALL total_stok(@hasil) menyalin nilai awal (NULL, karena OUT selalu mulai dari NULL) ke dalam parameter p_total, lalu prosedur menjalankan SELECT SUM(stok) INTO p_total yang menaruh angka 105 ke parameter tersebut. Saat prosedur selesai, nilai p_total disalin balik ke variabel @hasil milik pemanggil. Baris terakhir SELECT @hasil AS total_stok membuktikan angka tersebut memang sampai ke luar prosedur.

Penting dipahami bahwa CALL dan SELECT @hasil harus berada dalam sesi yang sama — variabel @ hidup selama koneksi terbuka dan tidak pernah dibawa ke koneksi lain. Kalau Anda menjalankan CALL lalu membuka jendela klien baru untuk membaca @hasil, hasilnya pasti NULL.

Parameter INOUT: Masuk sebagai Nilai, Keluar sebagai Hasil #

INOUT menggabungkan dua arah: nilai argumen masuk ke prosedur, diproses, lalu hasilnya dituliskan balik ke variabel yang sama. Prosedur gandakan_stok menggandakan angka yang diterimanya:

sql
DROP PROCEDURE IF EXISTS gandakan_stok;
DELIMITER $$
CREATE PROCEDURE gandakan_stok(INOUT p_angka INT)
BEGIN
  SET p_angka = p_angka * 2;
END$$
DELIMITER ;
SET @nilai = 20;
CALL gandakan_stok(@nilai);
SELECT @nilai AS nilai_setelah_gandakan;

Output yang Dihasilkan:

text
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)

Query OK, 0 rows affected (0.00 sec)

+------------------------+
| nilai_setelah_gandakan |
+------------------------+
|                     40 |
+------------------------+
1 row in set (0.00 sec)

Variabel @nilai berisi 20 ketika prosedur dipanggil, dan berubah menjadi 40 setelahnya — nilai yang sama berfungsi sebagai masukan sekaligus penampung keluaran. Sekali lagi ketiga statement berada dalam satu rangkaian sesi yang sama, sesuai aturan hidup variabel @.

Prosedur dengan Beberapa Parameter #

Prosedur boleh memiliki parameter sebanyak yang dibutuhkan, ditulis berurutan dipisah koma. Prosedur atur_harga menerima dua parameter IN: id produk dan harga baru, lalu memperbarui barisnya dan melaporkan jumlah baris yang berubah:

sql
DROP PROCEDURE IF EXISTS atur_harga;
DELIMITER $$
CREATE PROCEDURE atur_harga(IN p_id INT, IN p_harga DECIMAL(10,2))
BEGIN
  UPDATE produk SET harga = p_harga WHERE id = p_id;
  SELECT ROW_COUNT() AS baris_diubah;
END$$
DELIMITER ;
CALL atur_harga(2, 27000.00);
CALL atur_harga(99, 10000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| baris_diubah |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| baris_diubah |
+--------------+
|            0 |
+--------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Pemanggilan pertama mengubah harga Teh Melati menjadi 27000.00 dan melaporkan 1 baris; pemanggilan kedua memakai id 99 yang tidak ada sehingga ROW_COUNT() melaporkan 0 — laporan jujur tanpa pesan error, karena UPDATE pada id kosong bukanlah kegagalan.

Validasi Input di dalam Prosedur #

Parameter membuat prosedur terbuka terhadap nilai sembarangan, dan data rusak biasanya masuk lewat pintu ini. MySQL menyediakan SIGNAL untuk menolak nilai yang tidak masuk akal dengan pesan error kita sendiri — topik lengkapnya menyusul di chapter Error Handling, tetapi pola dasarnya layak dikenali sekarang:

sql
DROP PROCEDURE IF EXISTS tambah_stok;
DELIMITER $$
CREATE PROCEDURE tambah_stok(IN p_id INT, IN p_jumlah INT)
BEGIN
  IF p_jumlah < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Jumlah penambahan stok tidak boleh negatif';
  END IF;
  UPDATE produk SET stok = stok + p_jumlah WHERE id = p_id;
  SELECT ROW_COUNT() AS baris_diubah;
END$$
DELIMITER ;
CALL tambah_stok(1, 5);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| baris_diubah |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Pemanggilan valid menambah 5 stok Kopi Gayo dan melaporkan satu baris berubah. Ketika pemanggil mencoba nilai negatif, prosedur menolaknya keras:

sql
-- @error
CALL tambah_stok(1, -5);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1644 (45000): Jumlah penambahan stok tidak boleh negatif
Bye

Server membalas ERROR 1644 dengan pesan Jumlah penambahan stok tidak boleh negatif — pesan yang persis kita tulis, bukan pesan standar MySQL. Angka 45000 adalah SQLSTATE khusus aplikasi: kode yang disediakan untuk error buatan kita sendiri, dan kode 1644 adalah nomor error MySQL untuk kondisi tak tertangani tersebut.

Argumen Wajib: Default Tidak Ada #

Parameter prosedur tidak punya nilai bawaan. Seluruh parameter yang dideklarasikan wajib diisi saat CALL; kurang satu saja langsung ditolak:

sql
-- @error
CALL tambah_stok(1);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1318 (42000): Incorrect number of arguments for PROCEDURE belajar_sp.tambah_stok; expected 2, got 1
Bye

Pesan ERROR 1318 menyebut jumlah argumen yang diharapkan dan yang diterima, sehingga kesalahan pemanggilan mudah dilacak. Berlebih juga sama-sama ditolak — CALL tambah_stok(1, 5, 9) menghasilkan ERROR 1318 dengan pola yang sama. Jika sebuah nilai benar-benar boleh kosong, jangan mengandalkan default yang tidak ada; tandai parameter sebagai boleh NULL lalu tangani NULL di dalam prosedur dengan IF p_jumlah IS NULL THEN ....

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Prosedur kita kini punya pintu masuk dan pintu keluar: IN mengantarkan masukan, OUT mengembalikan keluaran lewat variabel @, dan INOUT keduanya sekaligus. Di dalam tubuh prosedur, kita juga mulai memakai SET dan SELECT ... INTO untuk mengolah nilai. Chapter berikutnya membedah dua mekanisme penyimpanan nilai tersebut secara penuh — variabel lokal DECLARE versus variabel sesi @ — lengkap dengan aturan scope yang menentukan kapan sebuah nama boleh dipakai.