crystal-db adalah pustaka akses basis data generik untuk Crystal yang menyediakan satu antarmuka untuk banyak mesin basis data, sedangkan detail koneksi diserahkan kepada driver seperti crystal-sqlite3 dan crystal-pg. Topik ini membangun proyek kecil bernama catatan-suhu yang menyimpan pembacaan sensor suhu ke basis data SQLite, mulai dari pemasangan dependensi, eksekusi query, transaksi, sampai connection pooling, yaitu pengelolaan sekumpulan koneksi yang dipinjam berulang. Driver PostgreSQL dibahas singkat pada bagian akhir sebagai gambaran penggunaan driver lain.
Dependensi proyek basis data#
Akses basis data dimulai dari dua shard yang dideklarasikan pada shard.yml. Shard pertama bernama db memuat antarmuka generik crystal-db, sedangkan shard kedua bernama sqlite3 memuat driver yang menerjemahkan antarmuka tersebut ke basis data SQLite. Nama kunci untuk pustaka antarmukanya adalah db, bukan crystal-db, karena nama shard pada repositorinya memang db. Simpan berkas berikut sebagai shard.yml pada folder proyek.
name: catatan-suhu
version: "0.1.0"
dependencies:
db:
github: crystal-lang/crystal-db
sqlite3:
github: crystal-lang/crystal-sqlite3
crystal: ">= 1.0.0"Dependensi dipasang dengan shards install yang dijalankan di folder proyek, yaitu folder tempat shard.yml berada. Blok berikut menghapus sisa pemasangan sebelumnya terlebih dahulu agar contoh selalu berulang dari keadaan bersih.
# bersihkan sisa pemasangan sebelumnya agar contoh berulang dari keadaan bersih
rm -rf lib shard.lock .shards
# pasang dependensi proyek
shards installI: Resolving dependencies
I: Fetching https://github.com/crystal-lang/crystal-db.git
I: Fetching https://github.com/crystal-lang/crystal-sqlite3.git
I: Installing db (0.15.0)
I: Installing sqlite3 (0.24.0)
I: Writing shard.lockAwalan I: menandai baris informasi dari shards. Baris Fetching menunjukkan repositori yang diunduh, baris Installing menunjukkan versi yang terpasang, yaitu db 0.15.0 dan sqlite3 0.24.0, sedangkan baris Writing shard.lock merekam versi persis tersebut. Hasil pemasangan berada di folder lib/ yang dibaca compiler saat require dijalankan.
Membuka koneksi dan menjalankan query#
Koneksi dibuka dengan DB.open yang menerima URI basis data. Skema sqlite3: pada URI menentukan driver yang digunakan, sedangkan sisa URI menunjuk berkas basis data yang dibuat di folder kerja apabila belum ada. Bentuk blok dari DB.open menutup koneksi secara otomatis ketika bloknya selesai, sehingga koneksi tidak tertinggal terbuka. Metode exec menjalankan pernyataan tanpa hasil seperti CREATE TABLE dan INSERT, sedangkan tanda tanya pada teks SQL adalah placeholder yang diisi oleh argumen berikutnya, sehingga nilai tidak pernah digabungkan langsung ke teks SQL.
Berkas berikut membuat tabel suhu, mengisi dua baris, membacanya kembali, lalu menghitung jumlah barisnya.
require "db"
require "sqlite3"
DB.open "sqlite3:./catatan.db" do |db|
db.exec "CREATE TABLE IF NOT EXISTS suhu (waktu TEXT, sensor TEXT, nilai INTEGER)"
db.exec "DELETE FROM suhu"
db.exec "INSERT INTO suhu VALUES (?, ?, ?)", "07:00", "ruang-utama", 24
db.exec "INSERT INTO suhu VALUES (?, ?, ?)", "07:00", "gudang", 19
db.query "SELECT waktu, sensor, nilai FROM suhu ORDER BY sensor" do |rs|
rs.each do
waktu = rs.read(String)
sensor = rs.read(String)
nilai = rs.read(Int32)
puts "#{waktu} #{sensor} #{nilai}"
end
end
jumlah = db.scalar("SELECT COUNT(*) FROM suhu")
puts "Jumlah catatan: #{jumlah}"
endPernyataan DELETE FROM suhu mengosongkan tabel sebelum pengisian, sehingga program dapat dijalankan berulang tanpa menduplikasi baris. Metode query menjalankan SELECT dan memberikan objek rs yang bertipe DB::ResultSet kepada blok. Setiap putaran rs.each membaca satu baris, dan rs.read mengambil nilai kolom berurutan sesuai daftar kolom pada SELECT, dengan tipe yang dinyatakan sebagai argumen seperti String dan Int32. Metode scalar mengembalikan nilai kolom pertama dari baris pertama, sehingga cocok untuk query agregat seperti COUNT. Jalankan program berikut.
# kompilasi dan jalankan program akses basis data
crystal run catatan.cr07:00 gudang 19
07:00 ruang-utama 24
Jumlah catatan: 2Dua baris pertama adalah isi tabel yang diurutkan oleh ORDER BY sensor, yaitu gudang terlebih dahulu karena abjadnya lebih awal. Baris terakhir berasal dari db.scalar yang menghitung dua baris hasil INSERT.
Transaksi#
Transaksi mengelompokkan beberapa pernyataan agar semuanya diterapkan bersama atau tidak sama sekali. Metode db.transaction menerima blok yang menerima objek transaksi, dan pernyataan di dalam blok dijalankan melalui tx.connection.exec agar masuk ke transaksi yang sedang berjalan, bukan ke koneksi lain dari pool. Ketika blok selesai tanpa exception, semua pernyataannya di-commit. Ketika exception terjadi di dalam blok, semua pernyataannya di-rollback secara otomatis, kemudian exception dilempar ulang ke pemanggil.
Berkas berikut memindahkan saldo 25 dari ali ke budi dalam satu transaksi, lalu menjalankan transaksi kedua yang sengaja digagalkan di tengah eksekusi.
require "db"
require "sqlite3"
DB.open "sqlite3:./rekening.db" do |db|
db.exec "CREATE TABLE IF NOT EXISTS rekening (nama TEXT, saldo INTEGER)"
db.exec "DELETE FROM rekening"
db.exec "INSERT INTO rekening VALUES (?, ?)", "ali", 100
db.exec "INSERT INTO rekening VALUES (?, ?)", "budi", 50
db.transaction do |tx|
tx.connection.exec "UPDATE rekening SET saldo = saldo - 25 WHERE nama = ?", "ali"
tx.connection.exec "UPDATE rekening SET saldo = saldo + 25 WHERE nama = ?", "budi"
end
puts "Setelah commit: saldo budi = #{db.scalar("SELECT saldo FROM rekening WHERE nama = 'budi'")}"
begin
db.transaction do |tx|
tx.connection.exec "UPDATE rekening SET saldo = saldo - 100 WHERE nama = ?", "budi"
raise "saldo tidak cukup"
end
rescue e
puts "Transaksi dibatalkan: #{e.message}"
end
puts "Setelah rollback: saldo budi = #{db.scalar("SELECT saldo FROM rekening WHERE nama = 'budi'")}"
end# kompilasi dan jalankan program transaksi
crystal run transaksi.crSetelah commit: saldo budi = 75
Transaksi dibatalkan: saldo tidak cukup
Setelah rollback: saldo budi = 75Baris pertama membuktikan transaksi pertama di-commit, sehingga saldo budi naik dari 50 menjadi 75. Transaksi kedua mengurangi saldo budi sebanyak 100 sebelum melempar exception, tetapi rollback otomatis membatalkan pengurangan itu, sehingga saldo budi tetap 75 pada baris terakhir.
Connection pooling#
Membuka koneksi basis data berbiaya lebih besar daripada menjalankan query, sehingga crystal-db mengelola pool di balik setiap DB.open. Pool menyimpan koneksi yang sudah terbuka dan meminjamkannya ke kode yang membutuhkan, lalu menerimanya kembali saat penggunaan selesai. Perilaku pool diatur melalui parameter pada URI. Nilai bawaannya adalah initial_pool_size sebesar 1, max_pool_size sebesar 0 yang berarti tanpa batas, dan checkout_timeout sebesar 5.0 detik.
Berkas berikut membatasi pool menjadi dua koneksi dengan max_pool_size=2 dan memperpendek batas tunggu menjadi 0.5 detik. Metode using_connection meminjam satu koneksi dan mengembalikannya ketika bloknya selesai.
require "db"
require "sqlite3"
DB.open "sqlite3:./pool.db?max_pool_size=2&checkout_timeout=0.5" do |db|
db.exec "CREATE TABLE IF NOT EXISTS pengaturan (kunci TEXT, nilai TEXT)"
db.using_connection do |c1|
db.using_connection do |c2|
puts "Dua koneksi dipinjam bersamaan: #{!c1.same?(c2)}"
end
end
db.using_connection do |c1|
db.using_connection do |c2|
begin
db.using_connection do |c3|
puts "Koneksi ketiga diperoleh"
end
rescue e : DB::PoolTimeout
puts "#{e.class}: #{e.message}"
end
end
end
end# kompilasi dan jalankan program pool koneksi
crystal run pool.crDua koneksi dipinjam bersamaan: true
DB::PoolTimeout: Could not check out a connection in 0.5 secondsBaris pertama membuktikan dua koneksi dipinjam secara bersamaan, karena !c1.same?(c2) bernilai true hanya apabila keduanya objek yang berbeda. Pada baris kedua, koneksi ketiga diminta ketika kedua koneksi masih dipinjam, sehingga permintaan menunggu sampai checkout_timeout terlampaui dan exception DB::PoolTimeout dilempar.
Driver PostgreSQL#
Driver PostgreSQL untuk crystal-db adalah crystal-pg yang diambil dari repositori will/crystal-pg. Pola pemakaiannya sama dengan driver SQLite, yaitu dependensi ditambahkan ke shard.yml, pustakanya di-require dengan require "pg", lalu URI koneksi menggunakan skema postgres:// yang memuat pengguna, kata sandi, host, port, dan nama basis data. Metode exec, query, scalar, dan transaction tidak berubah karena semuanya disediakan oleh crystal-db, sedangkan driver hanya menyediakan koneksi dan penerjemahan tipe data. Sebagian driver juga menerjemahkan placeholder tanda tanya ke format parameternya masing-masing.
# tidak-dijalankan
require "pg"
DB.open "postgres://budi:rahasia@localhost:5432/catatan" do |db|
db.exec "INSERT INTO suhu VALUES (?, ?, ?)", "07:00", "ruang-utama", 24
puts db.scalar("SELECT COUNT(*) FROM suhu")
endKesalahan umum#
- Kompilasi berhenti dengan pesan
Error: can't find file 'db'. Dependensi belum dipasang atau compiler dijalankan di luar folder proyek. Jalankanshards installdi folder yang memuat shard.yml sebelum kompilasi. - shards menolak shard.yml dengan pesan
E: Error shard name (db) doesn't match dependency name (crystal-db). Penyebabnya adalah dependensi dituliskan dengan kuncicrystal-db, sedangkan nama shard pada repositorinya adalahdb. Gunakan kuncidbpada bagian dependencies. - Program berhenti dengan pesan
no such table: suhu (SQLite3::Exception). Tabel belum dibuat pada basis data yang ditunjuk URI. Jalankan CREATE TABLE, misalnya dengan bentuk IF NOT EXISTS, sebelum query yang menyebut tabel itu. - Program diam selama lima detik lalu melempar
DB::PoolTimeout. Pool kehabisan koneksi dancheckout_timeoutbelum diatur, sehingga nilai bawaan 5.0 detik digunakan. Aturcheckout_timeoutpada URI atau pastikan setiap koneksi dikembalikan, misalnya dengan menutup blokusing_connection.
Ringkasan#
- crystal-db menyediakan antarmuka generik berupa
DB.open,exec,query,scalar, dantransaction, sedangkan driver seperti crystal-sqlite3 dan crystal-pg menyediakan implementasinya. - Placeholder tanda tanya mengisi parameter query dengan aman, dan
rs.readmembaca kolom hasil query secara berurutan dengan tipe yang dinyatakan. db.transactionmelakukan commit di akhir blok dan rollback otomatis apabila exception terjadi di dalamnya.- Pool diatur melalui parameter URI seperti
max_pool_sizedancheckout_timeout, dan melemparDB::PoolTimeoutapabila koneksi tidak diperoleh sebelum batas waktu.
Referensi#
- crystal-db — crystal-lang — Dokumentasi resmi crystal-db yang mencakup DB.open, transaksi, dan opsi pool.
- crystal-sqlite3 — crystal-lang — Repositori driver SQLite versi 0.24.0 yang dipasang dan dijalankan pada topik ini.
- crystal-pg — will — Repositori driver PostgreSQL untuk crystal-db yang dicontohkan pada bagian akhir.