Konsep driver telah kita pahami; kini saatnya memakai driver yang paling mudah dijangkau. SQLite adalah database relasional yang berjalan tertanam di dalam proses aplikasi — tanpa server terpisah, tanpa instalasi, tanpa konfigurasi. Node.js menyertakannya sebagai core module node:sqlite, sehingga kita bisa membuat database sungguhan, lengkap dengan tabel dan query SQL, hanya bermodal satu baris import. Chapter ini membahas cara membuka database, perbedaan exec dan prepare, tiga method eksekusi run, get, dan all, serta cara menyimpan data ke berkas yang bertahan antar proses.
Module yang Terpasang di Dalam Runtime #
Module node:sqlite ditambahkan ke Node.js pada versi 22.5.0 sebagai fitur eksperimental yang harus diaktifkan dengan flag --experimental-sqlite. Sejak versi 23.4.0 (dan 22.13.0 pada LTS 22), module ini dapat dipakai tanpa flag. Course ini memakai Node.js 24, tempat module tersebut berstatus release candidate dan siap dipakai langsung tanpa peringatan apa pun — seluruh contoh pada bab-bab database berikutnya memanfaatkan hal ini.
Pintu masuknya adalah class DatabaseSync. Satu instance mewakili satu koneksi ke satu database:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');String ':memory:' memberi tahu SQLite untuk hidup sepenuhnya di memori — database seperti ini cepat, bersih, dan terhapus otomatis saat db.close() dipanggil. Sebagai gantinya, kita bisa memberi nama berkas seperti 'toko.db', lalu seluruh data tersimpan permanen di filesystem.
exec untuk Struktur, prepare untuk Data #
Method db.exec(sql) mengeksekusi satu atau banyak statement SQL langsung dari string mentah. Method ini cocok untuk DDL — CREATE TABLE, CREATE INDEX, atau skrip struktur awal — karena bisa menjalankan beberapa statement sekaligus dan tidak menerima parameter. Method db.prepare(sql) berperan berbeda: method ini mengompilasi tepat satu statement menjadi prepared statement yang bisa dijalankan berulang kali dengan parameter berbeda. Pemisahan ini bukan gaya-gayaan — hanya prepare yang mendukung parameter binding, fondasi keamanan query yang kita bahas tuntas pada Chapter Query Parameterized. Rujukan lengkap sintaks SQL yang dipahami SQLite — dari SELECT hingga JOIN — tersedia di sqlite.org/lang.html
.
Ketiga method eksekusi di object hasil prepare() membagi tugas dengan jelas:
| Method | Mengembalikan | Dipakai untuk |
|---|---|---|
run(...parameter) | { changes, lastInsertRowid } | INSERT, UPDATE, DELETE |
get(...parameter) | Object satu baris pertama, atau undefined | Query yang hasilnya satu baris |
all(...parameter) | Array seluruh baris | Query dengan banyak hasil |
Nilai changes melaporkan berapa baris terdampak, sedangkan lastInsertRowid menyimpan id baris yang baru disisipkan — angka ini sangat berguna setelah INSERT pada tabel ber-PRIMARY KEY otomatis. Berikut contoh lengkapnya dalam satu program toko kecil:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga INTEGER NOT NULL,
stok INTEGER NOT NULL DEFAULT 0
)
`);
const sisip = db.prepare('INSERT INTO produk (nama, harga, stok) VALUES (?, ?, ?)');
sisip.run('Kopi Gayo', 85000, 10);
sisip.run('Teh Melati', 45000, 25);
sisip.run('Gula Aren', 30000, 0);
const hasil = sisip.run('Kopi Toraja', 95000, 8);
console.log('Produk baru -> id:', hasil.lastInsertRowid, '| baris bertambah:', hasil.changes);
console.log('Semua produk:', JSON.stringify(db.prepare('SELECT * FROM produk ORDER BY id').all()));
console.log('Cari id 2 :', JSON.stringify(
db.prepare('SELECT nama, harga FROM produk WHERE id = ?').get(2)
));
console.log('Stok kosong :', JSON.stringify(
db.prepare('SELECT nama FROM produk WHERE stok = ?').all(0)
));
const ubah = db.prepare('UPDATE produk SET stok = stok + ? WHERE nama = ?');
console.log('Tambah stok :', JSON.stringify(ubah.run(5, 'Gula Aren')));
const hapus = db.prepare('DELETE FROM produk WHERE id = ?');
console.log('Hapus id 2 :', JSON.stringify(hapus.run(2)));
console.log('Stok akhir :', JSON.stringify(
db.prepare('SELECT id, nama, stok FROM produk ORDER BY id').all()
));
db.close();Output yang Dihasilkan:
Produk baru -> id: 4 | baris bertambah: 1
Semua produk: [{"id":1,"nama":"Kopi Gayo","harga":85000,"stok":10},{"id":2,"nama":"Teh Melati","harga":45000,"stok":25},{"id":3,"nama":"Gula Aren","harga":30000,"stok":0},{"id":4,"nama":"Kopi Toraja","harga":95000,"stok":8}]
Cari id 2 : {"nama":"Teh Melati","harga":45000}
Stok kosong : [{"nama":"Gula Aren"}]
Tambah stok : {"changes":1,"lastInsertRowid":4}
Hapus id 2 : {"changes":1,"lastInsertRowid":4}
Stok akhir : [{"id":1,"nama":"Kopi Gayo","stok":10},{"id":3,"nama":"Gula Aren","stok":5},{"id":4,"nama":"Kopi Toraja","stok":8}]Mari kita bedah bagian-bagian pentingnya:
- Baris 5-12: tabel dibuat lewat
db.execdalam satu template literal multi-baris — bentuk yang nyaman untuk DDL karenaexecmenerima banyak statement. - Baris 14-19 (
const sisip = db.prepare(...)): statementINSERTdikompilasi sekali, lalu dipakai empat kali — tiga pemanggilan biasa pada baris 15-17 dan satu pemanggilan pada baris 19 yang hasilnya disimpan. Tanda tanya adalah positional parameter yang diisi sesuai urutan. - Baris 20 (
hasil.lastInsertRowid,hasil.changes): baris keempat mendapatid4 dariPRIMARY KEYotomatis, danchangesbernilai 1 karena satu baris bertambah. - Baris 24-26 (
.get(2)): query denganWHERE id = ?mengembalikan tepat satu object;getmengambil baris pertama tanpa membungkusnya dalam array. - Baris 28-30 (
.all(0)): parameter tidak harus string — angka juga terikat dengan aman ke placeholder. - Baris 32-36:
UPDATEdanDELETEsama-sama dilaporkan lewatchanges. Perhatikan bahwalastInsertRowidpada output keduanya masih menunjukINSERTterakhir (id 4), bukan baris yang diubah atau dihapus — field ini hanya relevan setelahINSERT.
Menyimpan Data ke Berkas #
Database :memory: terhapus begitu proses berakhir. Untuk data yang harus bertahan, cukup ganti argumen menjadi nama berkas — SQLite membuat berkas tersebut secara otomatis bila belum ada:
import { rmSync } from 'node:fs';
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('catatan.db');
db.exec(`
CREATE TABLE IF NOT EXISTS catatan (
id INTEGER PRIMARY KEY,
isi TEXT NOT NULL
)
`);
db.prepare('INSERT INTO catatan (isi) VALUES (?)').run('Belajar node:sqlite hari ini');
console.log('Isi tabel:', JSON.stringify(db.prepare('SELECT * FROM catatan').all()));
db.close();
rmSync('catatan.db', { force: true });
console.log('Berkas catatan.db dihapus setelah selesai');Output yang Dihasilkan:
Isi tabel: [{"id":1,"isi":"Belajar node:sqlite hari ini"}]
Berkas catatan.db dihapus setelah selesaiDua detail pada contoh ini layak dicatat. Pertama, CREATE TABLE IF NOT EXISTS menjaga program tetap aman dijalankan berulang kali pada berkas yang sama — tanpa klausal ini, eksekusi kedua gagal karena tabel sudah ada. Kedua, berkas catatan.db sengaja dihapus di akhir agar contoh ini selalu mulai dari keadaan bersih; pada aplikasi nyata kita justru menjaga berkas tersebut agar data bertahan.
Kita kini memiliki database yang bisa dibuat, diisi, dan dibaca tanpa keluar dari runtime. Seluruh keterampilan ini — SQL, parameter binding, siklus koneksi — akan terpakai ulang saat kita menyambungkan Node.js ke database client-server yang sesungguhnya, dimulai dari PostgreSQL pada Chapter Koneksi PostgreSQL dengan pg.