Binding ke Python di Chapter sebelumnya menyelesaikan masalah “siapa yang memanggil kode kita”; sekarang kita bahas kebutuhan yang jauh lebih umum: menyimpan data secara terstruktur tanpa memasang server database. SQLite adalah database SQL penuh yang hidup di dalam satu file biasa — tanpa proses server, tanpa konfigurasi, tanpa port jaringan — dan karena itulah ia ada di mana-mana: browser menyimpan riwayat dan cookie di SQLite, Android dan iPhone menyimpan data aplikasi di SQLite, dan hampir semua aplikasi desktop yang menyimpan “sesuatu yang terstruktur” juga memakainya. Pada Chapter ini kita pelajari pola C API-nya — open, prepare, bind, step, finalize — lalu membangun wrapper RAII mini di atasnya dan menjalankannya sungguhan dengan library SQLite yang tersedia di lingkungan course.
Database Embedded vs Database Client-Server #
Perbedaan mendasarnya ada pada arsitektur proses. PostgreSQL atau MySQL berjalan sebagai server tersendiri; program Anda berbicara dengannya lewat koneksi jaringan dan kredensial, dan server itulah yang menulis ke disk. SQLite berjalan di dalam proses program Anda: fungsi C yang Anda panggil langsung membaca dan menulis file database. Tidak ada jaringan berarti tidak ada latensi per query, tidak ada kredensial yang bocor, dan deployment cuma satu file data. Harga yang dibayar juga jelas: hanya satu proses yang boleh menulis pada satu waktu, tidak ada manajemen user, dan tidak ada replikasi bawaan.
| Situasi | Pilihan yang wajar |
|---|---|
| Aplikasi desktop/mobile menyimpan data lokal | SQLite |
| Cache lokal, log terstruktur, file konfigurasi kompleks | SQLite |
| Web app multi-server dengan ribuan penulis konkuren | PostgreSQL/MySQL |
| Butuh user management, replikasi, koneksi jaringan | PostgreSQL/MySQL |
| Analitik satu mesin atas data besar sekali | DuckDB/kolom-based, atau SQLite bila pas |
Aturan praktisnya: bila data dan program hidup di mesin yang sama dan penulisnya satu proses, SQLite hampir selalu pilihan paling sederhana yang tetap memberi jaminan transaksi penuh (ACID — atomik, konsisten, terisolasi, tahan crash).
Pola C API: open, prepare, bind, step, finalize #
SQLite menawarkan C API, jadi semua pengetahuan dari Chapter C ABI dan C++ ABI berlaku: kita memanggil fungsi C polos dengan handle pointer. Urutan panggilannya selalu sama lima langkah:
sqlite3* db = nullptr;
sqlite3_open("koperasi.db", &db); // 1) buka koneksi
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db, // 2) siapkan statement
"SELECT nama, saldo FROM anggota WHERE saldo > ?",
-1, &stmt, nullptr);
sqlite3_bind_int(stmt, 1, 100000); // 3) isi parameter '?'
while (sqlite3_step(stmt) == SQLITE_ROW) { // 4) tarik baris demi baris
const unsigned char* nama = sqlite3_column_text(stmt, 0);
int saldo = sqlite3_column_int(stmt, 1);
std::printf("%s: %d\n", nama, saldo);
}
sqlite3_finalize(stmt); // 5) bebaskan statement
sqlite3_close(db); // 6) tutup koneksi
Cara membangunnya di mesin Anda biasa adalah satu flag linker tambahan:
g++ -std=c++23 -Wall -Wextra program.cpp -lsqlite3 -o programHarness course ini mengompilasi contoh dengan perintah tetap tanpa flag tambahan, sehingga program teruji di bawah memuat libsqlite3.so.0 saat runtime dengan dlopen — teknik yang sudah Anda kenal dari Chapter Static dan Dynamic Libraries. Fungsi yang dipanggil dan urutannya identik dengan pola di atas; hanya cara menemukannya yang berbeda. Perhatikan juga state machine di langkah 4: sqlite3_step mengembalikan SQLITE_ROW setiap kali ada baris baru dan SQLITE_DONE saat data habis — loop while hanyalah penerjemahan langsung dari kontrak itu.
Wrapper RAII Mini yang Teruji #
Polanya jelas, tetapi lima langkah manual berarti lima peluang lupa finalize atau close ketika exception terjadi. Versi RAII berikut memindahkan setiap pembersihan ke destructor, dan program ini benar-benar kita kompilasi dan jalankan:
// sqlite-mini.cpp — wrapper RAII mini di atas C API SQLite sungguhan.
// Harness course mengompilasi contoh tanpa -lsqlite3, sehingga library
// dimuat saat runtime dengan dlopen; urutan fungsi open/prepare/bind/
// step/finalize-nya persis sama dengan pemakaian biasa.
#include <dlfcn.h>
#include <sqlite3.h>
#include <cstdio>
#include <stdexcept>
#include <string>
// Kumpulan pointer fungsi yang diambil dari libsqlite3.so.0 saat runtime.
struct Api {
decltype(&sqlite3_open) open;
decltype(&sqlite3_close) close;
decltype(&sqlite3_prepare_v2) prepare;
decltype(&sqlite3_bind_int) bind_int;
decltype(&sqlite3_bind_text) bind_text;
decltype(&sqlite3_step) step;
decltype(&sqlite3_column_int) column_int;
decltype(&sqlite3_column_text) column_text;
decltype(&sqlite3_finalize) finalize;
decltype(&sqlite3_errmsg) errmsg;
};
Api muatSqlite() {
void* lib = dlopen("libsqlite3.so.0", RTLD_NOW);
if (!lib) throw std::runtime_error(dlerror());
Api api{};
api.open = reinterpret_cast<decltype(api.open)>(dlsym(lib, "sqlite3_open"));
api.close = reinterpret_cast<decltype(api.close)>(dlsym(lib, "sqlite3_close"));
api.prepare = reinterpret_cast<decltype(api.prepare)>(dlsym(lib, "sqlite3_prepare_v2"));
api.bind_int = reinterpret_cast<decltype(api.bind_int)>(dlsym(lib, "sqlite3_bind_int"));
api.bind_text = reinterpret_cast<decltype(api.bind_text)>(dlsym(lib, "sqlite3_bind_text"));
api.step = reinterpret_cast<decltype(api.step)>(dlsym(lib, "sqlite3_step"));
api.column_int = reinterpret_cast<decltype(api.column_int)>(dlsym(lib, "sqlite3_column_int"));
api.column_text = reinterpret_cast<decltype(api.column_text)>(dlsym(lib, "sqlite3_column_text"));
api.finalize = reinterpret_cast<decltype(api.finalize)>(dlsym(lib, "sqlite3_finalize"));
api.errmsg = reinterpret_cast<decltype(api.errmsg)>(dlsym(lib, "sqlite3_errmsg"));
return api;
}
// RAII untuk koneksi: destructor memanggil sqlite3_close secara otomatis.
class Database {
public:
Database(const Api& api, const char* path) : api_(&api) {
if (api.open(path, &db_) != SQLITE_OK)
throw std::runtime_error("sqlite3_open gagal");
}
~Database() { api_->close(db_); }
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
sqlite3* get() const { return db_; }
const Api& api() const { return *api_; }
private:
const Api* api_;
sqlite3* db_ = nullptr;
};
// RAII untuk prepared statement: destructor memanggil sqlite3_finalize.
class Statement {
public:
Statement(const Database& db, const char* sql) : api_(&db.api()) {
if (api_->prepare(db.get(), sql, -1, &stmt_, nullptr) != SQLITE_OK)
throw std::runtime_error(api_->errmsg(db.get()));
}
~Statement() { api_->finalize(stmt_); }
Statement(const Statement&) = delete;
Statement& operator=(const Statement&) = delete;
void bind(int indeks, int nilai) { api_->bind_int(stmt_, indeks, nilai); }
void bind(int indeks, const std::string& teks) {
api_->bind_text(stmt_, indeks, teks.c_str(), -1, SQLITE_TRANSIENT);
}
bool step() { return api_->step(stmt_) == SQLITE_ROW; } // true = ada baris
int columnInt(int kolom) const { return api_->column_int(stmt_, kolom); }
std::string columnText(int kolom) const {
return reinterpret_cast<const char*>(api_->column_text(stmt_, kolom));
}
private:
const Api* api_;
sqlite3_stmt* stmt_ = nullptr;
};
int main() {
Api api = muatSqlite();
Database db(api, ":memory:"); // database di RAM, hilang saat program usai
{ // DDL: jalankan sekali sampai SQLITE_DONE; tidak menghasilkan baris
Statement ddl(db, "CREATE TABLE kota (id INTEGER PRIMARY KEY, nama TEXT, populasi INTEGER)");
ddl.step();
}
const char* sqlInsert = "INSERT INTO kota (nama, populasi) VALUES (?, ?)";
const char* nama[] = {"Jakarta", "Bandung", "Surabaya"};
const int populasi[] = {10600000, 2500000, 2900000};
for (int i = 0; i < 3; ++i) {
Statement ins(db, sqlInsert);
ins.bind(1, std::string(nama[i]));
ins.bind(2, populasi[i]);
ins.step();
}
std::printf("3 baris dimasukkan ke tabel kota\n\n");
Statement query(db, "SELECT id, nama, populasi FROM kota ORDER BY id");
std::printf("id | nama | populasi\n");
while (query.step()) {
std::printf("%d | %-8s | %d\n", query.columnInt(0),
query.columnText(1).c_str(), query.columnInt(2));
}
return 0;
}Output yang Dihasilkan:
3 baris dimasukkan ke tabel kota
id | nama | populasi
1 | Jakarta | 10600000
2 | Bandung | 2500000
3 | Surabaya | 2900000Bedah bagian pentingnya:
- Baris 12-24 (
struct Api) — setiap pointer fungsi mengikuti tipe persis darisqlite3.hlewatdecltype(&sqlite3_...), sehingga salah tanda tangan tertangkap compiler, bukan saat runtime. - Baris 27-41 (
muatSqlite) —dlopenmemuat library,dlsymmengambil alamat tiap fungsi; pada proyek biasa tahap ini digantikan oleh-lsqlite3saat link. - Baris 44-65 (
class Database) — RAII untuk koneksi: constructor membuka, destructor memanggilsqlite3_close; copy di-deletekarena menyalin handle berarti dua destructor untuk satu koneksi (double close). - Baris 68-104 (
class Statement) — RAII untuk prepared statement:preparedi constructor,finalizedi destructor; dua overloadbindmenyembunyikan perbedaansqlite3_bind_intdansqlite3_bind_text. - Baris 110-113 (
Database db(api, ":memory:")) — path khusus:memory:membuat database di RAM: cepat, tanpa file sisa, ideal untuk contoh dan unit test. - Baris 131-137 (loop insert) — satu prepared statement dipersiapkan per baris di contoh ini; pada data besar, siapkan sekali lalu panggil
sqlite3_resetdi antara baris agar kompilasi SQL tidak diulang. - Baris 141-147 (loop query) —
step()membungkus state machine:trueberartiSQLITE_ROWdan kolom boleh dibaca;SQLITE_TRANSIENTpadabind_textmemberi tahu SQLite untuk menyalin string kita segera, sehingga aman meski buffer aslinya berubah.
Dengan wrapper ini, program C++ Anda punya penyimpanan relasional penuh tanpa operasi apa pun di luar prosesnya sendiri — data di tangan, tanpa server. Namun tidak semua data hidup di mesin sendiri; sering kali program justru harus berbicara dengan dunia luar, dan itulah topik Chapter berikutnya: socket dan jaringan.
Referensi: An Introduction To The SQLite C/C++ Interface , SQLite Documentation .