Program Python yang memakai SQLite pada dasarnya berputar pada dua objek: Connection dan Cursor. Kebingungan membedakan keduanya adalah sumber galat klasik — misalnya lupa commit() (tugas Connection) atau memakai cursor yang sudah kehabisan hasil (sifat Cursor). Chapter ini membedah pembagian kerja keduanya, cara mengambil hasil query, sampai perilaku blok with yang sering disalahpahami sebagai penutup koneksi.

Pembagian Kerja: Connection dan Cursor #

Fungsi sqlite3.connect() membuka satu sesi ke database dan mengembalikan objek Connection. Dari koneksi tersebut kita membuat objek Cursor untuk mengeksekusi statement SQL. Tanggung jawab keduanya berbeda tegas:

ObjekBertanggung jawab atas
ConnectionMembuka/menutup sesi, transaction (commit(), rollback()), pengaturan koneksi
CursorMengeksekusi SQL (execute(), executemany()), mengambil hasil (fetchone(), fetchall())

connect() menerima beberapa parameter yang sering kita perlukan nanti:

ParameterKegunaanNilai bawaan
databasePath berkas database atau ":memory:"wajib diisi
timeoutLama menunggu saat database terkunci (detik)5.0
isolation_levelMode transaction; None berarti autocommit""
row_factoryFungsi pembentuk baris hasil — dibahas pada Chapter Row FactoryNone

Sebagai pintas, Connection juga punya metode execute() sendiri: setiap panggilan con.execute() membuat cursor baru sebentar, menjalankan satu statement, lalu mengembalikannya. Untuk contoh pendek, con.execute(...) cukup; untuk beberapa statement berurutan, cursor eksplisit lebih rapi.

fetchone, fetchall, dan Iterasi Cursor #

Setelah execute() sebuah SELECT, hasil belum otomatis tampil di layar — cursor menyimpan hasilnya dan kita menariknya sesuai kebutuhan. Tiga metode utamanya: fetchone() mengambil satu baris berikutnya (atau None jika habis), fetchall() mengambil sisa baris sekaligus, dan iterasi for mengambil baris satu per satu.

python
import sqlite3

con = sqlite3.connect(":memory:")
cur = con.cursor()

cur.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL)")
cur.executemany(
    "INSERT INTO pelanggan (nama) VALUES (?)",
    [("Budi",), ("Siti",), ("Ani",)],
)

cur.execute("SELECT id, nama FROM pelanggan ORDER BY id")
print(cur.fetchone())
print(cur.fetchone())
print(cur.fetchall())

con.close()

Output yang Dihasilkan:

text
(1, 'Budi')
(2, 'Siti')
[(3, 'Ani')]

Mari kita bedah perilakunya:

  1. Baris 12 (cur.execute("SELECT ...")): cursor menjalankan SELECT; hasil menunggu di cursor, belum ada yang diambil.
  2. Baris 13 (print(cur.fetchone())): baris pertama ditarik dan dikembalikan sebagai tuple (1, 'Budi').
  3. Baris 14 (print(cur.fetchone())): baris kedua ditarik — posisi pembacaan sudah maju, tidak mengulang dari awal.
  4. Baris 15 (print(cur.fetchall())): sisa hasil diambil sebagai list; karena dua baris pertama sudah ditarik, hanya tersisa satu.

Cursor bersifat sekali pakai: setelah hasil habis, cursor tidak bisa “di-rewind”. Iterasi for langsung pada cursor memanfaatkan sifat ini untuk menghemat memori — baris diproses satu per satu tanpa memuat semuanya:

python
import sqlite3

con = sqlite3.connect(":memory:")
cur = con.cursor()

cur.execute("CREATE TABLE pesan (isi TEXT)")
cur.executemany("INSERT INTO pesan VALUES (?)", [("halo",), ("selamat datang",)])

cur.execute("SELECT isi FROM pesan ORDER BY isi")
print("iterasi pertama:", [baris[0] for baris in cur])
print("iterasi kedua  :", [baris[0] for baris in cur])

cur.execute("SELECT isi FROM pesan ORDER BY isi")
print("setelah execute ulang:", [baris[0] for baris in cur])

con.close()

Output yang Dihasilkan:

text
iterasi pertama: ['halo', 'selamat datang']
iterasi kedua  : []
setelah execute ulang: ['halo', 'selamat datang']

Iterasi kedua menghasilkan list kosong karena cursor pertama sudah menghabiskan seluruh hasilnya. Menjalankan execute() ulang dengan query yang sama mengisi cursor dengan hasil segar, sehingga iterasi ketiga berhasil lagi.

Beberapa Cursor pada Satu Koneksi #

Satu Connection boleh memiliki banyak Cursor aktif sekaligus, dan semuanya berbagi database yang sama. Ini berguna saat satu bagian kode sedang menelusuri hasil query sementara bagian lain perlu menjalankan statement lain:

python
import sqlite3

con = sqlite3.connect(":memory:")
cur1 = con.cursor()
cur2 = con.cursor()

cur1.execute("CREATE TABLE pesan (isi TEXT)")
cur1.execute("INSERT INTO pesan VALUES (?)", ("halo",))
cur1.execute("INSERT INTO pesan VALUES (?)", ("selamat datang",))

cur2.execute("SELECT isi FROM pesan ORDER BY isi")
print(cur2.fetchall())

con.close()

Output yang Dihasilkan:

text
[('halo',), ('selamat datang',)]

Cursor cur1 menulis dua baris, lalu cursor cur2 membacanya — keduanya menempel pada koneksi yang sama sehingga saling melihat perubahan satu sama lain.

Membaca Nama Kolom: description #

Hasil fetchall() berupa tuple tanpa nama kolom. Atribut cursor.description menyimpan metadata kolom hasil query terakhir, dan elemen pertama tiap entry adalah namanya:

python
import sqlite3

con = sqlite3.connect(":memory:")
cur = con.cursor()

cur.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL)")
cur.execute("INSERT INTO pelanggan (nama) VALUES (?)", ("Budi",))

cur.execute("SELECT id, nama FROM pelanggan")
print(cur.description)
print([kolom[0] for kolom in cur.description])

con.close()

Output yang Dihasilkan:

text
(('id', None, None, None, None, None, None), ('nama', None, None, None, None, None, None))
['id', 'nama']

Enam elemen None setelah nama kolom adalah kolom metadata warisan DB-API (tipe, ukuran, dan sejenisnya) yang tidak diisi SQLite. Untuk sekadar nama kolom, ambil elemen ke-nol saja — atau lebih nyaman lagi, pakai sqlite3.Row pada Chapter Row Factory yang membuat kolom bisa diakses langsung berdasarkan nama.

Objek Connection sebaiknya ditutup dengan con.close() agar berkas terlepas dan sumber daya kembali. Python juga menyediakan bentuk with untuk koneksi — tetapi perilakunya sering disalahpahami:

python
import os
import sqlite3

if os.path.exists("toko.db"):
    os.remove("toko.db")

con = sqlite3.connect("toko.db")

with con:
    con.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL)")
    con.execute("INSERT INTO pelanggan (nama) VALUES (?)", ("Budi",))

print(con.execute("SELECT COUNT(*) FROM pelanggan").fetchone())

try:
    with con:
        con.execute("INSERT INTO pelanggan (nama) VALUES (?)", ("Siti",))
        raise ValueError("simulasi galat aplikasi")
except ValueError as e:
    print("tertangkap:", e)

print(con.execute("SELECT COUNT(*) FROM pelanggan").fetchone())

con.close()

try:
    con.execute("SELECT 1")
except sqlite3.ProgrammingError as e:
    print("ProgrammingError:", e)

Output yang Dihasilkan:

text
(1,)
tertangkap: simulasi galat aplikasi
(1,)
ProgrammingError: Cannot operate on a closed database.

Urutan peristiwanya:

  1. Blok with con: pertama: blok berjalan sukses, jadi keluar dari blok memanggil con.commit() — INSERT Budi tersimpan. Hitungan tercetak (1,) dari koneksi yang masih terbuka.
  2. Blok with con: kedua: raise ValueError di dalam blok membuat keluar blok memanggil con.rollback() — INSERT Siti dibatalkan, hitungan tetap (1,).
  3. con.close(): koneksi benar-benar ditutup.
  4. con.execute("SELECT 1") setelahnya: operasi pada koneksi tertutup memicu sqlite3.ProgrammingError.

Dengan koneksi dan cursor di tangan, kita bisa berpindah ke topik yang menyentuh keamanan sekaligus kenyamanan: mengirim nilai ke query lewat placeholder, bukan merangkai string SQL.