kodingup.camp

Beranda/Node.js Backend: Database, Auth, Keamanan & Deploy

Dasar Database

Modul 1 dari 108 menit bacaDatabase Fundamentals

Isi modul

    Kenapa ini penting

    Bayangkan warung kopi yang mencatat pesanan di papan tulis. Selama warung buka, catatannya terbaca. Begitu ada yang menghapus papan (server restart), semua catatan lenyap. Itulah yang terjadi pada array const todos = [] di bagian 1. Aplikasi sungguhan butuh tempat menyimpan data yang tetap ada walaupun server mati, bisa dicari dengan cepat walaupun isinya jutaan baris, dan tetap rapi walaupun ratusan orang menulis di saat yang sama.

    Tempat itu namanya database, dan program yang mengelolanya disebut DBMS (Database Management System), misalnya PostgreSQL, MySQL, SQLite, atau MongoDB. Hampir semua pekerjaan backend pada akhirnya berputar di sekitar database. Login adalah mencocokkan data di tabel user, checkout adalah menulis ke tabel pesanan sambil mengurangi stok, dan laporan penjualan adalah menjumlahkan isi tabel. Kalau kamu paham database, setengah pekerjaan backend sudah kamu kuasai.

    Konsep inti

    SQL vs NoSQL

    Ada dua keluarga besar database:

    SQL (relasional) NoSQL (dokumen, key-value, dll.)
    Bentuk data Tabel dengan kolom tetap, seperti spreadsheet yang ketat Dokumen JSON bebas bentuk, atau pasangan kunci-nilai
    Contoh PostgreSQL, MySQL, SQLite, SQL Server MongoDB, Redis, DynamoDB, Firestore
    Relasi antar data Kuat, lewat foreign key dan JOIN Biasanya data ditanam (embed) atau di-join manual di kode
    Skema Wajib didefinisikan dulu Fleksibel, bisa berubah per dokumen
    Cocok untuk Toko online, keuangan, sistem dengan data saling terhubung Cache, data log besar, data yang bentuknya sangat bervariasi

    Untuk pemula dan untuk sebagian besar aplikasi bisnis, mulailah dengan SQL. Data toko online (user, produk, pesanan, item pesanan) sangat saling terhubung, dan database relasional memang dirancang untuk itu. Database relasional juga memberi jaminan ACID: transaksi terjadi utuh atau tidak sama sekali (Atomic), data selalu memenuhi aturan (Consistent), transaksi yang berjalan bersamaan tidak saling merusak (Isolated), dan yang sudah tersimpan tidak hilang walau listrik padam (Durable). NoSQL bukan "lebih modern", hanya alat untuk masalah yang berbeda. Redis, misalnya, sering dipakai di samping PostgreSQL sebagai cache.

    Tabel, baris, kolom, dan primary key

    Database relasional menyimpan data dalam tabel. Satu tabel untuk satu jenis "benda": pelanggan, produk, pesanan. Setiap baris (row) adalah satu benda, setiap kolom (column) adalah satu atribut dengan tipe tertentu (INTEGER, TEXT, dan sebagainya).

    Setiap baris butuh tanda pengenal unik yang tidak pernah berubah, namanya primary key. Biasanya berupa kolom id berisi angka yang naik otomatis (auto increment). Analoginya seperti nomor KTP: nama bisa sama, alamat bisa pindah, tapi nomornya tetap.

    Relasi dan foreign key

    Pesanan selalu milik seorang pelanggan. Daripada menyalin nama dan kota pelanggan ke setiap pesanan, kita cukup menyimpan id pelanggannya di kolom pelanggan_id. Kolom yang menunjuk ke primary key tabel lain disebut foreign key. Database lalu bisa menjaga aturan "tidak boleh ada pesanan untuk pelanggan yang tidak ada".

    Ada tiga pola relasi:

    Memecah data ke tabel-tabel yang saling menunjuk seperti ini disebut normalisasi. Tujuannya supaya setiap fakta disimpan di satu tempat saja. Kalau pelanggan pindah kota, kamu cukup mengubah satu baris, bukan ratusan pesanan.

    Empat perintah dasar SQL (CRUD)

    SQL (Structured Query Language) adalah bahasa untuk "bertanya" ke database relasional. Empat perintah ini mewakili operasi CRUD (Create, Read, Update, Delete):

    Operasi SQL Contoh
    Create INSERT INSERT INTO pelanggan (nama, kota) VALUES ('Andi', 'Bandung')
    Read SELECT SELECT nama FROM pelanggan WHERE kota = 'Bandung' ORDER BY nama
    Update UPDATE UPDATE pesanan SET total = 200000 WHERE id = 2
    Delete DELETE DELETE FROM pesanan WHERE id = 1

    Perhatikan WHERE di UPDATE dan DELETE. Tanpa WHERE, perintah itu berlaku untuk semua baris di tabel. Ini salah satu kesalahan paling mahal di dunia backend.

    JOIN: menggabungkan tabel

    Karena data dipecah ke beberapa tabel, kita butuh cara menyatukannya lagi saat dibaca. Itulah JOIN:

    Ditambah GROUP BY dan fungsi agregat seperti COUNT() dan SUM(), kamu bisa membuat laporan seperti "total belanja per pelanggan" dalam satu query.

    SQLite untuk belajar, PostgreSQL untuk produksi

    SQLite adalah database yang hidup di dalam satu file (atau bahkan di RAM). Tidak ada server terpisah yang harus dipasang dan dijalankan. Ini membuatnya sempurna untuk belajar, untuk tes otomatis, dan bahkan untuk aplikasi produksi skala kecil-menengah dengan satu server. Kabar baiknya, Node.js sekarang punya modul node:sqlite bawaan. Di dokumentasi Node 24 dan 26, statusnya Stability 1.2 (Release candidate) sejak v24.15.0. Artinya API-nya sudah hampir final dan tidak lagi butuh flag --experimental-sqlite, tapi belum berlabel "Stable" penuh. Saat dicoba di Node 24.22.1 dan 26.0.0, modul ini berjalan tanpa peringatan apa pun.

    PostgreSQL (sering disingkat Postgres) adalah server database penuh. Ia berjalan sebagai proses terpisah, menerima banyak koneksi sekaligus, punya tipe data kaya (JSONB, array, timestamp dengan zona waktu), dan bisa melayani banyak server aplikasi sekaligus. Hampir semua platform hosting (Railway, Render, Fly.io, Supabase, Neon) menyediakan Postgres siap pakai. Aturan praktisnya:

    Situasi Pilihan
    Belajar, prototipe, tes otomatis SQLite
    Satu server, trafik sedang, mau simpel SQLite (dengan mode WAL dan backup rutin)
    Banyak server aplikasi, banyak penulis bersamaan, butuh fitur lanjutan PostgreSQL

    Karena SQL-nya hampir sama, pindah dari SQLite ke Postgres nanti tidak terlalu menyakitkan, apalagi kalau kamu memakai ORM (Modul 2).

    Contoh kode

    Contoh ini memakai node:sqlite bawaan, jadi tidak perlu npm install apa pun. Simpan sebagai sql-dasar.js di folder yang package.json-nya berisi "type": "module", lalu jalankan node sql-dasar.js.

    // sql-dasar.js, latihan SQL pakai SQLite bawaan Node (tanpa install apa pun)
    import { DatabaseSync } from 'node:sqlite';
    
    const db = new DatabaseSync(':memory:'); // database di RAM, hilang saat program selesai
    db.exec('PRAGMA foreign_keys = ON');     // SQLite baru menegakkan foreign key kalau ini dinyalakan
    
    db.exec(`
      CREATE TABLE pelanggan (
        id    INTEGER PRIMARY KEY AUTOINCREMENT,
        nama  TEXT NOT NULL,
        kota  TEXT NOT NULL
      );
      CREATE TABLE pesanan (
        id           INTEGER PRIMARY KEY AUTOINCREMENT,
        pelanggan_id INTEGER NOT NULL REFERENCES pelanggan(id),
        produk       TEXT NOT NULL,
        total        INTEGER NOT NULL
      );
    `);
    
    // INSERT, pakai placeholder ? (di Modul 2 kita bahas kenapa ini WAJIB)
    const tambahPelanggan = db.prepare('INSERT INTO pelanggan (nama, kota) VALUES (?, ?)');
    tambahPelanggan.run('Andi', 'Bandung');
    tambahPelanggan.run('Sari', 'Surabaya');
    tambahPelanggan.run('Joko', 'Bandung');
    
    const tambahPesanan = db.prepare('INSERT INTO pesanan (pelanggan_id, produk, total) VALUES (?, ?, ?)');
    tambahPesanan.run(1, 'Kopi Gayo 250g', 85000);
    tambahPesanan.run(1, 'French Press', 210000);
    tambahPesanan.run(2, 'Kopi Toraja 250g', 95000);
    
    // SELECT dengan WHERE dan ORDER BY
    console.log(db.prepare('SELECT nama FROM pelanggan WHERE kota = ? ORDER BY nama').all('Bandung'));
    // → [ [Object: null prototype] { nama: 'Andi' }, [Object: null prototype] { nama: 'Joko' } ]
    
    // UPDATE, hasil .run() memberi tahu berapa baris yang berubah
    console.log(db.prepare('UPDATE pesanan SET total = total - 10000 WHERE id = ?').run(2));
    // → { changes: 1, lastInsertRowid: 3 }
    
    // DELETE
    console.log(db.prepare('DELETE FROM pesanan WHERE total < ?').run(90000).changes);
    // → 1   (Kopi Gayo 85000 terhapus)
    
    // JOIN, gabungkan dua tabel lewat kolom relasi
    console.log(db.prepare(`
      SELECT p.nama, o.produk, o.total
      FROM pesanan o
      JOIN pelanggan p ON p.id = o.pelanggan_id
      ORDER BY o.total DESC
    `).all());
    // → [ { nama: 'Andi', produk: 'French Press', total: 200000 },
    //     { nama: 'Sari', produk: 'Kopi Toraja 250g', total: 95000 } ]
    
    // LEFT JOIN + GROUP BY, semua pelanggan, termasuk yang belum pernah belanja
    console.log(db.prepare(`
      SELECT p.nama, COUNT(o.id) AS jumlah, COALESCE(SUM(o.total), 0) AS belanja
      FROM pelanggan p
      LEFT JOIN pesanan o ON o.pelanggan_id = p.id
      GROUP BY p.id
      ORDER BY belanja DESC
    `).all());
    // → [ { nama: 'Andi', jumlah: 1, belanja: 200000 },
    //     { nama: 'Sari', jumlah: 1, belanja: 95000 },
    //     { nama: 'Joko', jumlah: 0, belanja: 0 } ]
    
    // foreign key menolak pesanan untuk pelanggan yang tidak ada
    try {
      tambahPesanan.run(99, 'Hantu', 1000);
    } catch (e) {
      console.log(e.message); // → FOREIGN KEY constraint failed
    }
    db.close();
    

    Beberapa hal yang patut diperhatikan dari output asli di atas. [Object: null prototype] muncul karena node:sqlite mengembalikan objek tanpa prototype. Isinya tetap bisa dibaca biasa (baris.nama). Lalu UPDATE mengembalikan changes: 1, angka yang nanti sering kamu pakai untuk mendeteksi "data tidak ditemukan". Terakhir, COALESCE(SUM(o.total), 0) mengganti NULL (Joko belum belanja) menjadi 0.

    Untuk Postgres, SQL-nya hampir sama. Perbedaan yang paling sering ditemui: auto increment ditulis id SERIAL PRIMARY KEY atau id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, dan placeholder di driver pg ditulis $1, $2 alih-alih ?.

    // contoh-pg.js, query yang sama ke PostgreSQL dengan driver pg 8.23.1
    import pg from 'pg';
    
    const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
    // DATABASE_URL contoh: postgres://user:password@localhost:5432/toko
    const { rows } = await pool.query('SELECT nama FROM pelanggan WHERE kota = $1', ['Bandung']);
    console.log(rows); // → [ { nama: 'Andi' }, { nama: 'Joko' } ]
    await pool.end();
    

    Pool menyimpan beberapa koneksi yang siap dipakai ulang. Membuka koneksi baru ke Postgres untuk setiap request itu lambat, jadi server selalu memakai pool.

    Kesalahan umum

    1. UPDATE atau DELETE tanpa WHERE. Satu baris SQL bisa mengubah seluruh tabel. Biasakan menulis WHERE lebih dulu, dan coba query-nya sebagai SELECT dengan WHERE yang sama untuk melihat baris mana yang akan kena.
    2. Lupa PRAGMA foreign_keys = ON di SQLite. Secara bawaan SQLite tidak menegakkan foreign key demi kompatibilitas lama. Tanpa pragma ini, pesanan untuk pelanggan id 99 akan diterima begitu saja.
    3. Menyimpan uang sebagai angka desimal (float). 0.1 + 0.2 di JavaScript menghasilkan 0.30000000000000004. Simpan rupiah sebagai INTEGER (dalam rupiah atau sen), atau pakai tipe NUMERIC di Postgres.
    4. Menyalin data ke banyak tabel. Menyimpan nama_pelanggan di tabel pesanan membuat data tidak sinkron saat nama berubah. Simpan id-nya, lalu JOIN saat membaca. Pengecualian yang disengaja: harga saat dibeli memang harus disalin ke item pesanan, karena harga produk bisa berubah besok.
    5. Memilih NoSQL hanya karena "tidak perlu skema". Skema tetap ada, hanya pindah ke kode aplikasimu dan tidak dijaga database. Untuk data yang saling terhubung, database relasional jauh lebih aman.

    Latihan

    1. Mudah, Dari contoh di atas, tulis query untuk menampilkan nama dan kota semua pelanggan yang bukan dari Bandung, urut berdasarkan nama.
    2. Sedang, Tambahkan tabel produk (id, nama, harga), lalu ubah tabel pesanan supaya menyimpan produk_id (foreign key), bukan nama produk. Tulis JOIN tiga tabel yang menampilkan nama pelanggan, nama produk, harga.
    3. Menantang, Buat query "pelanggan terbaik": nama pelanggan dan total belanjanya, hanya untuk pelanggan dengan total belanja di atas 100.000, urut dari yang terbesar. Petunjuk: filter hasil agregat pakai HAVING, bukan WHERE.
    Lihat solusi

    1. <> atau != berarti "tidak sama dengan".

    console.log(db.prepare('SELECT nama, kota FROM pelanggan WHERE kota <> ? ORDER BY nama').all('Bandung'));
    // → [ { nama: 'Sari', kota: 'Surabaya' } ]
    

    2. Tabel produk dibuat lebih dulu karena pesanan menunjuk ke sana.

    db.exec(`
      CREATE TABLE produk (id INTEGER PRIMARY KEY AUTOINCREMENT, nama TEXT NOT NULL, harga INTEGER NOT NULL);
      CREATE TABLE pesanan2 (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        pelanggan_id INTEGER NOT NULL REFERENCES pelanggan(id),
        produk_id INTEGER NOT NULL REFERENCES produk(id)
      );
      INSERT INTO produk (nama, harga) VALUES ('Kopi Gayo', 85000), ('French Press', 210000);
      INSERT INTO pesanan2 (pelanggan_id, produk_id) VALUES (1, 1), (1, 2), (2, 1);
    `);
    console.log(db.prepare(`
      SELECT pl.nama AS pelanggan, pr.nama AS produk, pr.harga
      FROM pesanan2 o
      JOIN pelanggan pl ON pl.id = o.pelanggan_id
      JOIN produk pr ON pr.id = o.produk_id
    `).all());
    // → Andi/Kopi Gayo/85000, Andi/French Press/210000, Sari/Kopi Gayo/85000
    

    3. WHERE menyaring baris sebelum dikelompokkan, HAVING menyaring setelah agregat dihitung.

    console.log(db.prepare(`
      SELECT p.nama, SUM(o.total) AS belanja
      FROM pelanggan p
      JOIN pesanan o ON o.pelanggan_id = p.id
      GROUP BY p.id
      HAVING SUM(o.total) > ?
      ORDER BY belanja DESC
    `).all(100000));
    // → [ { nama: 'Andi', belanja: 200000 } ]
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Learn Basic SQL in 15 Minutes | Business Intelligence For Beginners | SQL Tutorial For Beginners 1/3Adam Finer - Learn BI · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.