Operator -> dan ->> pada chapter sebelumnya sebenarnya hanya pembuka dari sebuah bahasa kecil bernama JSON Path — sintaks untuk menunjuk lokasi mana pun di dalam dokumen JSON. Selama ini kita hanya memakai bentuk paling sederhana seperti $.warna dan $.tag[0], padahal path mampu menulusuri array bersarang, menjaring seluruh elemen sekaligus dengan wildcard, bahkan menjadi alamat balasan saat kita mencari nilai tertentu di dalam dokumen. Pada chapter ini kita membedah anatominya tuntas, menguji batas nyatanya di MySQL 8.4 — termasuk satu fitur yang sering dianggap ada padahal tidak: filter path — lalu menutup dengan tiga alat pemeriksaan keberadaan nilai dan path.
Menyiapkan Tabel Latihan #
Latihan kita tetap memakai produk(id, nama, atribut) di database belajar_json dengan tiga baris berstruktur beda. Sebagian besar contoh path juga memakai literal JSON langsung — cara tercepat bereksperimen tanpa menyentuh tabel:
DROP DATABASE IF EXISTS belajar_json;
CREATE DATABASE belajar_json;
USE belajar_json;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 AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
atribut JSON NOT NULL
);
INSERT INTO produk (nama, atribut) VALUES
('Kopi Gayo 250g', '{"warna": "coklat", "harga": 55000, "tag": ["arabika", "toraja"], "dimensi": {"tinggi": 12, "diameter": 8}}'),
('Mug Keramik', '{"warna": "putih", "harga": 45000, "tag": ["keramik", "microwave"], "dimensi": {"tinggi": 10, "diameter": 8}}'),
('Teh Melati 100g', '{"harga": 25000, "tag": ["teh", "melati"]}');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0Anatomi JSON Path #
Path ditulis sebagai satu string yang dibaca dari kiri ke kanan. Tanda dolar $ selalu menjadi titik berangkat dan melambangkan dokumen itu sendiri; setiap segmen setelahnya menuruni satu tingkat ke dalam dokumen. Tabel berikut merangkum seluruh segmen yang dikenali MySQL:
| Segmen | Makna | Contoh |
|---|---|---|
$ | Akar dokumen | '$' |
$.kunci | Anggota object bernama kunci | '$.harga' |
$.kunci1.kunci2 | Anggota bersarang | '$.dimensi.tinggi' |
$[n] | Elemen array indeks ke-n (mulai 0) | '$.tag[0]' |
$[last] | Elemen terakhir array | '$.tag[last]' |
$[*] | Semua elemen array | '$.tag[*]' |
$.* | Semua anggota object | '$.*' |
$[m][n] | Elemen bersarang dalam array of arrays | '$[1][2]' |
Mari kita verifikasi beberapa segmen tersebut pada array literal sederhana. Kali ini dokumen kita memang berupa array di akar, bukan object, sehingga indeks langsung menempel pada $:
SELECT
JSON_EXTRACT('[10, 20, 30]', '$') AS seluruh_array,
JSON_EXTRACT('[10, 20, 30]', '$[0]') AS elemen_pertama,
JSON_EXTRACT('[10, 20, 30]', '$[2]') AS elemen_terakhir,
JSON_EXTRACT('[10, 20, 30]', '$[*]') AS wildcard_semua;Output yang Dihasilkan:
+---------------+----------------+-----------------+----------------+
| seluruh_array | elemen_pertama | elemen_terakhir | wildcard_semua |
+---------------+----------------+-----------------+----------------+
| [10, 20, 30] | 10 | 30 | [10, 20, 30] |
+---------------+----------------+-----------------+----------------+
1 row in set (0.00 sec)Tiga kolom pertama lugas: path $ mengembalikan dokumen apa adanya, $[0] dan $[2] mengambil elemen pada indeksnya. Kolom keempat menampilkan 10, 20, dan 30 — wildcard [*] mengumpulkan seluruh elemen menjadi array hasil baru, sehingga tampilannya kebetulan sama dengan $. Perbedaannya akan terlihat jelas begitu wildcard digabung dengan penulusuran lebih dalam, seperti pada array bersarang di bawah ini.
Array Bersarang dan Penulusuran Dua Tingkat #
Dokumen dunia nyata sering memuat array di dalam array — misalnya daftar titik koordinat. Path menulusurinya dengan menuliskan indeks berurutan tanpa nama kunci:
SELECT
JSON_EXTRACT('[[10, 20], [30, 40, 50]]', '$[1]') AS array_kedua,
JSON_EXTRACT('[[10, 20], [30, 40, 50]]', '$[1][2]') AS elemen_dalam,
JSON_EXTRACT('[[10, 20], [30, 40, 50]]', '$[*][0]') AS awal_tiap_elemen;Output yang Dihasilkan:
+--------------+--------------+------------------+
| array_kedua | elemen_dalam | awal_tiap_elemen |
+--------------+--------------+------------------+
| [30, 40, 50] | 50 | [10, 30] |
+--------------+--------------+------------------+
1 row in set (0.00 sec)Path $[1] mengambil array kedua utuh, lalu $[1][2] menembus dua lapis: ambil elemen indeks 1, lalu elemen indeks 2 di dalamnya — hasilnya 50. Yang paling menarik adalah $[*][0]: wildcard [*] memilih setiap array di tingkat pertama, [0] lalu mengambil elemen pertama dari masing-masing, dan hasilnya [10, 30] — satu array berisi awal tiap elemen. Inilah pola penulusuran dua tingkat yang membuat wildcard berbeda dari sekadar mengambil array mentah.
Autowrap Ketika Path dan Bentuk Dokumen Tak Cocok #
Apa jadinya bila path berindeks $[0] dikenakan pada dokumen yang bukan array — misalnya angka tunggal? MySQL tidak menolaknya, melainkan membungkus nilai tunggal tersebut menjadi array satu elemen secara otomatis, sebuah perilaku bernama autowrap:
SELECT
JSON_EXTRACT('17', '$[0]') AS indeks_pada_skalar,
JSON_EXTRACT('17', '$') AS akar_skalar;Output yang Dihasilkan:
+--------------------+-------------+
| indeks_pada_skalar | akar_skalar |
+--------------------+-------------+
| 17 | 17 |
+--------------------+-------------+
1 row in set (0.00 sec)Kedua path menghasilkan 17: nilai skalar 17 dibungkus diam-diam menjadi [17] supaya $[0] punya sesuatu yang bisa diindeks. Perilaku yang sama berlaku ketika path bertitik dikenakan pada array — misalnya $.harga pada array hanya akan gagal menemukan anggota. Autowrap menjadikan path MySQL lunak terhadap ketidakcocokan bentuk: hasil tetap terbatas pada nilai yang masuk akal, sisanya berujung NULL.
Wildcard pada Object #
Wildcard untuk object adalah $.* — “semua anggota object ini”. Hasilnya dikemas menjadi array nilai, tanpa kunci:
SELECT
atribut->'$.*' AS semua_nilai
FROM produk
WHERE id = 3;Output yang Dihasilkan:
+----------------------------+
| semua_nilai |
+----------------------------+
| [["teh", "melati"], 25000] |
+----------------------------+
1 row in set (0.00 sec)Dokumen Teh Melati hanya memiliki dua anggota — tag dan harga pada urutan internal — dan $.* mengumpulkan kedua nilainya menjadi satu array: array tag tampil lebih dulu karena kunci tag lebih pendek, disusul angka harga. Kunci-kunci tersebut hilang dari hasil karena wildcard object memang mengambil nilai, bukan pasangan kunci-nilai. Kombinasi wildcard object dan array seperti $.dimensi.* juga sah; path tersebut mengembalikan array berisi seluruh anggota dari setiap object dimensi.
Kenyataan Filter Path: Tidak Didukung MySQL #
Sebagian besar pembaca yang pernah memakai JSONPath di bahasa lain mengenal filter seperti $[?(@.harga > 100)] — path yang menyaring elemen array berdasarkan kondisi. Banyak tutorial menganggap MySQL mendukungnya. Mari kita uji langsung di MySQL 8.4 dan lihat apa katanya:
-- @error
SELECT JSON_EXTRACT('[{"harga": 200}, {"harga": 50}, {"harga": 500}]', '$[?(@.harga > 100)]');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 3143 (42000): Invalid JSON path expression. The error is around character position 2.
ByeServer menolak path tersebut dengan ERROR 3143 — ungkapan path tidak sah — karena parser JSON Path MySQL memang tidak mengenal tanda tanya maupun sintaks filter. Penolakan ini terjadi pada semua fungsi JSON yang menerima path, jadi menulis filter dalam JSON_EXTRACT, JSON_SET, maupun JSON_SEARCH sama-sama gagal. Ini bukan kekurangan yang bisa diakali dengan trik penulisan; fiturnya memang tidak ada di MySQL 8.4.
Kebutuhan “saring elemen array” tetap bisa dipenuhi lewat jalur lain. Fungsi JSON_CONTAINS menguji keberadaan nilai tertentu, operator MEMBER OF memeriksa keanggotaan elemen, dan JSON_TABLE — yang kita bahas ringkas di chapter JSON Functions — mengubah array menjadi baris tabel sehingga klausa WHERE biasa bisa bekerja. Pemfilteran berbasis angka dalam jumlah besar paling efisien lewat kolom generated yang di-index, bahasan chapter tersendiri di depan.
JSON_SEARCH: Mencari Nilai, Mendapatkan Path #
Semua path di atas kita tulis sendiri. Fungsi JSON_SEARCH membalik arah kerjanya: ia menerima nilai yang dicari, lalu mengembalikan path tempat nilai itu ditemukan. Argumen kedua berisi mode 'one' (berhenti di temuan pertama) atau 'all' (kumpulkan semua), dan string pencarian boleh memakai karakter pengganti % serta _ sebagaimana LIKE:
SELECT
JSON_SEARCH('{"tag": ["arabika", "toraja"], "warna": "coklat"}', 'all', 'arabika') AS cari_arabika,
JSON_SEARCH('{"tag": ["arabika", "toraja"], "warna": "coklat"}', 'all', '%a%a%') AS cari_pola;Output yang Dihasilkan:
+--------------+--------------------------+
| cari_arabika | cari_pola |
+--------------+--------------------------+
| "$.tag[0]" | ["$.tag[0]", "$.tag[1]"] |
+--------------+--------------------------+
1 row in set (0.00 sec)Pencarian 'arabika' mengembalikan "$.tag[0]" — alamat lengkap elemen pertama array tag — sebagai string path tunggal. Pola '%a%a%' menemukan dua lokasi sekaligus, yaitu arabika dan toraja, sehingga hasilnya dikemas sebagai array berisi dua path. Terapkan pada tabel produk, fungsi ini menjawab “produk mana yang bertag keramik” dengan alamat persisnya:
SELECT
nama,
JSON_SEARCH(atribut, 'all', 'keramik') AS lokasi
FROM produk
ORDER BY id;Output yang Dihasilkan:
+-----------------+------------+
| nama | lokasi |
+-----------------+------------+
| Kopi Gayo 250g | NULL |
| Mug Keramik | "$.tag[0]" |
| Teh Melati 100g | NULL |
+-----------------+------------+
3 rows in set (0.00 sec)Hanya Mug Keramik yang memuat nilai keramik, dan lokasinya tercatat sebagai "$.tag[0]". Dua produk lain menghasilkan NULL karena nilainya tidak ditemukan — perilaku standar fungsi JSON terhadap ketiadaan. Path hasil JSON_SEARCH dapat langsung dipakai sebagai argumen fungsi lain, misalnya JSON_REMOVE untuk membuang nilai di lokasi tersebut.
Menguji Keberadaan: MEMBER OF dan JSON_CONTAINS_PATH #
Dua pertanyaan akhir yang sering muncul: “apakah nilai ini ada di array?” dan “apakah path ini ada di dokumen?”. Operator MEMBER OF menjawab yang pertama dengan menguji keanggotaan sebuah nilai dalam array JSON:
SELECT
nama,
'arabika' MEMBER OF (atribut->'$.tag') AS bertag_arabika
FROM produk
ORDER BY id;Output yang Dihasilkan:
+-----------------+----------------+
| nama | bertag_arabika |
+-----------------+----------------+
| Kopi Gayo 250g | 1 |
| Mug Keramik | 0 |
| Teh Melati 100g | 0 |
+-----------------+----------------+
3 rows in set (0.00 sec)Operator MEMBER OF mengembalikan 1 hanya untuk Kopi Gayo — satu-satunya produk yang memuat string "arabika" di array tag-nya. Perhatikan bahwa operand kiri adalah teks biasa 'arabika', sedangkan operand kanan harus berupa array JSON — hasil atribut->'$.tag' memang array, sehingga kombinasi ini pas.
Untuk keberadaan path, fungsi JSON_CONTAINS_PATH menguji satu atau beberapa path tanpa peduli nilainya. Argumen 'one' berarti “cukup satu dari path ini ada”, 'all' berarti “semua path harus ada”:
SELECT
nama,
JSON_CONTAINS_PATH(atribut, 'one', '$.warna') AS punya_warna,
JSON_CONTAINS_PATH(atribut, 'all', '$.warna', '$.dimensi') AS warna_dan_dimensi
FROM produk
ORDER BY id;Output yang Dihasilkan:
+-----------------+-------------+-------------------+
| nama | punya_warna | warna_dan_dimensi |
+-----------------+-------------+-------------------+
| Kopi Gayo 250g | 1 | 1 |
| Mug Keramik | 1 | 1 |
| Teh Melati 100g | 0 | 0 |
+-----------------+-------------+-------------------+
3 rows in set (0.00 sec)Kolom punya_warna bernilai 1 untuk Kopi Gayo dan Mug Keramik saja, sedangkan kolom warna_dan_dimensi menuntut kedua path hadir — Teh Melati yang hanya punya harga dan tag menghasilkan 0. Perbedaan JSON_CONTAINS_PATH dengan MEMBER OF terletak pada sasaran: yang pertama bertanya tentang alamat, yang kedua tentang isi.
Membersihkan Database Latihan #
Eksperimen path selesai dan database latihan kita jatuhkan:
DROP DATABASE belajar_json;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- The JSON Data Type — MySQL 8.4 Reference Manual
- JSON Search Functions — MySQL 8.4 Reference Manual
- JSON Function Reference — MySQL 8.4 Reference Manual
Anatomi JSON Path kini lengkap di tangan kita: akar, anggota, indeks, last, wildcard [*] dan .*, array bersarang, autowrap, plus fakta penting bahwa filter ?(...) tidak dikenali MySQL 8.4. Path tidak hanya berguna untuk membaca — fungsi-fungsi modifikasi juga menggunakannya sebagai alamat tujuan perubahan. Chapter berikutnya membedah JSON_SET, JSON_INSERT, JSON_REPLACE, dan JSON_REMOVE: empat fungsi yang mengubah isi dokumen di lokasi yang kita tunjuk.