Secara bawaan, modul sqlite3 mengembalikan setiap baris hasil sebagai tuple polos. Tidak ada nama kolom — kita mengakses baris[1], baris[2], dan seterusnya berdasarkan posisi. Selama query pendek, ini terasa ringkas; begitu SELECT memakai banyak kolom, JOIN, atau SELECT *, angka-angka itu menjadi sandi yang rapuh: satu kolom bergeser dan seluruh kode diam-diam membaca kolom yang salah. Row factory adalah mekanisme yang mengubah bentuk baris sebelum sampai ke kode kita.
Baris Tuple Bawaan dan Masalahnya #
Perhatikan pola akses posisional pada contoh berikut:
import sqlite3
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, kota TEXT)")
cur.executemany(
"INSERT INTO pelanggan (nama, kota) VALUES (?, ?)",
[("Budi", "Jakarta"), ("Siti", "Bandung")],
)
for baris in cur.execute("SELECT id, nama, kota FROM pelanggan ORDER BY id"):
print(baris[1], "tinggal di", baris[2])
con.close()Output yang Dihasilkan:
Budi tinggal di Jakarta
Siti tinggal di BandungKode tersebut bekerja, tetapi baris[1] dan baris[2] hanya benar selama daftar SELECT tetap id, nama, kota. Ubah query menjadi SELECT id, kota, nama, atau tambahkan satu kolom di tengah, dan tidak ada galat yang muncul — program hanya mencetak nilai yang salah. Kelas bug seperti ini justru yang paling mahal karena tidak pernah terlihat sebagai error.
sqlite3.Row: Kolom Berdasarkan Nama #
Kelas sqlite3.Row adalah bentuk baris yang paling sering dipakai. Kita memasangnya lewat atribut row_factory pada Connection (atau Cursor), dan sejak itu setiap hasil fetch berupa objek Row yang bisa diakses berdasarkan nama kolom maupun posisi:
import sqlite3
con = sqlite3.connect(":memory:")
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, kota TEXT)")
cur.executemany(
"INSERT INTO pelanggan (nama, kota) VALUES (?, ?)",
[("Budi", "Jakarta"), ("Siti", "Bandung")],
)
for baris in con.execute("SELECT id, nama, kota FROM pelanggan ORDER BY id"):
print(f"{baris['id']}: {baris['nama']} dari {baris['kota']}")
baris = cur.execute("SELECT id, nama, kota FROM pelanggan WHERE id = ?", (1,)).fetchone()
print(baris["nama"])
print(baris[1])
print(baris.keys())
print(len(baris))
print(tuple(baris))
con.close()Output yang Dihasilkan:
1: Budi dari Jakarta
2: Siti dari Bandung
Budi
Budi
['id', 'nama', 'kota']
3
(1, 'Budi', 'Jakarta')Mari kita bedah kemampuan objek Row dari baris-baris pengujian di atas:
- Baris 4 (
con.row_factory = sqlite3.Row): atribut di-set pada Connection, jadi semua cursor yang lahir dari koneksi ini — termasuk cursor implisit daricon.execute()— ikut memakainya. Atribut yang sama juga bisa dipasang per-cursor:cur.row_factory = sqlite3.Row, boleh dikerjakan kapan saja sebelum hasilnya di-fetch. - Baris 13 (
baris['id'],baris['nama'],baris['kota']): akses berdasarkan nama kolom — query boleh mengubah urutan SELECT tanpa merusak kode. - Baris 18 (
print(baris[1])): akses posisional tetap tersedia, berguna saat berpindah dari kode tuple lama secara bertahap. - Baris 19 (
baris.keys()): daftar nama kolom hasil query — berguna untuk debugging dan membuat dict sendiri. - Baris 20-21 (
len(baris),tuple(baris)):Rowtahu berapa kolomnya dan bisa dikembalikan menjadi tuple biasa saat dibutuhkan.
Row Factory Buatan Sendiri #
row_factory pada dasarnya adalah fungsi: dipanggil dengan dua argumen (cursor dan baris tuple mentah), dan bebas mengembalikan bentuk apa pun. Fungsi pembuat dictionary berikut menggabungkan cur.description — metadata nama kolom dari Chapter Koneksi dan Cursor — dengan nilai baris:
import sqlite3
def baris_dict(cur, row):
return {kolom[0]: nilai for kolom, nilai in zip(cur.description, row)}
con = sqlite3.connect(":memory:")
con.row_factory = baris_dict
cur = con.cursor()
cur.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, kota TEXT)")
cur.executemany(
"INSERT INTO pelanggan (nama, kota) VALUES (?, ?)",
[("Budi", "Jakarta"), ("Siti", "Bandung")],
)
hasil = cur.execute("SELECT id, nama, kota FROM pelanggan ORDER BY id").fetchall()
print(hasil)
print(hasil[1]["kota"])
con.close()Output yang Dihasilkan:
[{'id': 1, 'nama': 'Budi', 'kota': 'Jakarta'}, {'id': 2, 'nama': 'Siti', 'kota': 'Bandung'}]
BandungBedah singkat fungsinya:
- Signature
(cur, row): modulsqlite3memanggil fungsi ini sekali per baris dengan cursor yang mengeksekusi query dan satu baris tuple mentah. cur.description: daftar metadata kolom; elemen ke-nol tiap entry adalah nama kolom, sejajar satu-ke-satu dengan nilai dirow.zip(cur.description, row): memasangkan setiap metadata kolom dengan nilainya, lalu comprehension membangun dictionary{nama_kolom: nilai}.
Memilih Bentuk Baris #
Tiga bentuk baris yang sekarang kita kuasai punya wilayah kerja masing-masing:
| Bentuk | Contoh akses | Kelebihan | Kekurangan |
|---|---|---|---|
| tuple bawaan | baris[1] | paling cepat, tanpa konversi | rapuh terhadap perubahan urutan kolom |
sqlite3.Row | baris["nama"] | akses nama + posisi, keys(), kelas bawaan modul | objek khusus, bukan dictionary asli |
| dict buatan | baris["nama"] | dictionary murni, gampang diserialisasi | fungsi Python dipanggil tiap baris, paling lambat |
Satu pertunjukan terakhir yang kita perlukan sebelum menyusun aplikasi utuh: galat. Database bisa menolak query, menolak data, dan mengunci berkas — dan semuanya datang sebagai exception Python dengan hierarki tersendiri.