Penyimpanan berbasis berkas

Penyimpanan JSON dan kapan berkas saja sudah cukup.

Daftar isi

Penyimpanan berbasis berkas memakai sistem berkas sebagai media persistensi dengan format yang dibaca dan ditulis langsung oleh aplikasi, biasanya JSON. Pendekatan ini cukup untuk data berukuran kecil hingga menengah yang diakses oleh satu proses pada satu waktu, seperti catatan konfigurasi pengguna, daftar tugas pribadi, atau arsip sensor sederhana. Topik ini menjelaskan cara menulis dan membaca JSON dengan node:fs/promises, pola penulisan atomik untuk mencegah korupsi akibat proses berhenti di tengah operasi, serta kapan berkas saja sudah memadai dibandingkan basis data. Setelah membaca topik ini, pembaca dapat memutuskan kapan penyimpanan berbasis berkas tepat dipakai dan menulis kode yang aman terhadap penulisan setengah jadi.

Modul node:fs/promises menyediakan fungsi writeFile dan readFile yang berbasis promise, sehingga dapat dipakai dengan await di dalam fungsi async. Penulisan JSON dilakukan dengan JSON.stringify pada objek JavaScript, dan pembacaan dilakukan dengan JSON.parse pada teks berkas. Encoding 'utf8' wajib disebut pada readFile agar fungsi mengembalikan string dan bukan Buffer berisi byte mentah. Tanpa encoding, hasil berupa Buffer yang harus dikonversi lebih dulu sebelum dapat diuraikan sebagai JSON.

Berkas store.js berikut memperlihatkan siklus tulis, baca, tambah catatan, dan tulis ulang pada berkas db.json di direktori kerja saat ini. Objek yang ditulis berupa larik catatan dengan tiga field, yaitu id, nama, dan skor. Fungsi readJson membungkus readFile dan JSON.parse sehingga pemanggil tidak perlu mengulang dua langkah ini pada setiap pemakaian.

js store.js
const fs = require('node:fs/promises');
const path = require('node:path');

async function writeJsonAtomic(file, data) {
  const tmp = `${file}.tmp`;
  await fs.writeFile(tmp, JSON.stringify(data, null, 2));
  await fs.rename(tmp, file);
}

async function readJson(file) {
  const teks = await fs.readFile(file, 'utf8');
  return JSON.parse(teks);
}

async function main() {
  const file = path.join(process.cwd(), 'db.json');
  const awal = [
    { id: 1, nama: 'ali', skor: 85 },
    { id: 2, nama: 'citra', skor: 92 }
  ];
  await writeJsonAtomic(file, awal);
  console.log('ditulis:', awal.length, 'catatan');

  const baca = await readJson(file);
  console.log('dibaca:', JSON.stringify(baca));

  baca.push({ id: 3, nama: 'budi', skor: 78 });
  await writeJsonAtomic(file, baca);

  const baca2 = await readJson(file);
  console.log('setelah tambah:');
  console.log(JSON.stringify(baca2, null, 2));
}

main().catch((err) => console.error('error:', err.message));
text Keluaran
ditulis: 2 catatan
dibaca: [{"id":1,"nama":"ali","skor":85},{"id":2,"nama":"citra","skor":92}]
setelah tambah:
[
  {
    "id": 1,
    "nama": "ali",
    "skor": 85
  },
  {
    "id": 2,
    "nama": "citra",
    "skor": 92
  },
  {
    "id": 3,
    "nama": "budi",
    "skor": 78
  }
]

Pemanggilan JSON.stringify(baca) tanpa argumen indentasi menghasilkan JSON satu baris yang ringkas, sedangkan JSON.stringify(baca2, null, 2) menghasilkan JSON yang dipretty-print dengan indentasi dua spasi. Format ringkas cocok untuk penyimpanan internal karena menghemat byte, sedangkan format pretty-print cocok untuk berkas yang dibaca manusia atau dikomit ke kontrol versi sehingga diff antar revisi mudah dibaca.

Penulisan atomik#

Penulisan langsung dengan fs.writeFile(file, teks) menimpa berkas tujuan secara bertahap, sehingga bila proses berhenti di tengah operasi akibat crash, mati listrik, atau sinyal SIGKILL, berkas tujuan dapat berada pada keadaan setengah ditulis. Pembaca berkasnya berikutnya akan gagal menguraikan JSON karena parser menemukan akhir berkas yang tidak utuh. Pola yang mencegah kondisi ini adalah menulis ke berkas sementara lebih dahulu, lalu mengganti nama berkas sementara menjadi berkas tujuan dengan fs.rename.

Pemanggilan fs.rename bersifat atomik pada sistem berkas berbasis POSIX (standar Unix dan Linux) ketika berkas sumber dan tujuan berada pada sistem berkas yang sama. Akibatnya, pembaca yang membuka berkas tujuan baik sebelum maupun sesudah rename selalu melihat versi lengkap. Fungsi writeJsonAtomic pada store.js memakai pola ini dengan menulis ke ${file}.tmp lalu memanggil rename. Bila proses berhenti setelah writeFile tetapi sebelum rename, berkas tujuan tidak tersentuh dan berkas .tmp tertinggal, yang dapat dibersihkan pada startup atau dibiarkan saja karena tidak dipakai pada pembacaan utama.

Kapan berkas saja cukup#

Penyimpanan berbasis berkas tepat dipakai ketika sebagian besar kondisi berikut terpenuhi. Pertama, hanya satu proses yang menulis ke berkas pada satu waktu, sehingga tidak ada konflik penulisan. Kedua, ukuran berkas tetap kecil hingga menengah, biasanya di bawah beberapa megabita, sehingga pembacaan penuh ke memori tidak menjadi beban. Ketiga, skema data sederhana dan tidak memerlukan kueri berbasis indeks, sebab pencarian pada larik JSON yang dibaca penuh menjadi mahal ketika data tumbuh. Keempat, tidak ada kebutuhan transaksi yang menjangkau beberapa berkas atau beberapa operasi yang harus berhasil bersamaan.

Ketika kondisi ini tidak lagi terpenuhi, basis data menjadi pilihan yang lebih tepat. Basis data relasional seperti PostgreSQL menyediakan transaksi, indeks, dan konkurensi yang aman antar proses. Basis data dokumen seperti MongoDB menyediakan skema fleksibel dan kueri berbasis indeks. Pemilihan kedua jenis basis data ini dibahas pada topik selanjutnya. Penyimpanan berbasis berkas tetap relevan untuk cache sederhana, berkas konfigurasi yang ditulis pengguna, atau arsip cadangan yang dibuat sekali dan dibaca berkali-kali.

Kesalahan umum#

Pemakaian JSON.parse tanpa penanganan error adalah kesalahan paling mahal. Bila berkas rusak atau kosong, JSON.parse melempar SyntaxError yang membuat proses berhenti bila tidak ditangkap. Pembungkusan readJson dengan try/catch atau rantai .catch pada pemanggilan main() menangkap error dan memberi kesempatan untuk memulihkan keadaan, misalnya dengan mengganti dengan default dan mencatat peringatan.

Kesalahan lain adalah konkurensi penulisan. Dua proses yang menulis berkas yang sama pada waktu bersamaan dapat saling menimpa hasil, sebab pola baca-modifikasi-tulis (baca, ubah, lalu tulis ulang) tidak terkoordinasi. Untuk satu proses, urutan await cukup, tetapi lintas proses memerlukan kunci berbasis berkas (file lock) atau migrasi ke basis data yang menyediakan kunci pada level baris. Penyimpanan berbasis berkas tidak cocok untuk skenario banyak penulis konkuren, sebab kunci berkas sendiri sulit diimplementasikan dengan benar dan rawan deadlock, yaitu kebuntuan saat dua proses saling menunggu kunci yang dipegang pihak lain.

Ringkasan#

  • Penulisan JSON memakai JSON.stringify dengan fs.writeFile, sedangkan pembacaan memakai fs.readFile dengan encoding 'utf8' dan JSON.parse.
  • Penulisan atomik dilakukan dengan menulis ke berkas .tmp pada direktori yang sama lalu fs.rename menjadi berkas tujuan, yang atomik pada sistem berkas POSIX.
  • Penyimpanan berbasis berkas tepat untuk data kecil yang diakses satu proses pada satu waktu, tanpa kebutuhan transaksi lintas berkas.
  • Konkurensi penulisan dan ukuran berkas yang tumbuh menjadi tanda bahwa basis data relasional atau dokumen lebih sesuai.

Referensi#