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:

python
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:

text
Budi tinggal di Jakarta
Siti tinggal di Bandung

Kode 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:

python
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:

text
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:

  1. Baris 4 (con.row_factory = sqlite3.Row): atribut di-set pada Connection, jadi semua cursor yang lahir dari koneksi ini — termasuk cursor implisit dari con.execute() — ikut memakainya. Atribut yang sama juga bisa dipasang per-cursor: cur.row_factory = sqlite3.Row, boleh dikerjakan kapan saja sebelum hasilnya di-fetch.
  2. Baris 13 (baris['id'], baris['nama'], baris['kota']): akses berdasarkan nama kolom — query boleh mengubah urutan SELECT tanpa merusak kode.
  3. Baris 18 (print(baris[1])): akses posisional tetap tersedia, berguna saat berpindah dari kode tuple lama secara bertahap.
  4. Baris 19 (baris.keys()): daftar nama kolom hasil query — berguna untuk debugging dan membuat dict sendiri.
  5. Baris 20-21 (len(baris), tuple(baris)): Row tahu 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:

python
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:

text
[{'id': 1, 'nama': 'Budi', 'kota': 'Jakarta'}, {'id': 2, 'nama': 'Siti', 'kota': 'Bandung'}]
Bandung

Bedah singkat fungsinya:

  1. Signature (cur, row): modul sqlite3 memanggil fungsi ini sekali per baris dengan cursor yang mengeksekusi query dan satu baris tuple mentah.
  2. cur.description: daftar metadata kolom; elemen ke-nol tiap entry adalah nama kolom, sejajar satu-ke-satu dengan nilai di row.
  3. 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:

BentukContoh aksesKelebihanKekurangan
tuple bawaanbaris[1]paling cepat, tanpa konversirapuh terhadap perubahan urutan kolom
sqlite3.Rowbaris["nama"]akses nama + posisi, keys(), kelas bawaan modulobjek khusus, bukan dictionary asli
dict buatanbaris["nama"]dictionary murni, gampang diserialisasifungsi 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.