Seluruh contoh sejauh ini memanggil conn.execute() seolah connection mengerjakan semuanya. Kesan itu menyesatkan: sebenarnya ada dua objek yang bekerja sama — connection yang membuka database dan mengelola transaksi, serta cursor yang menjalankan SQL dan memegang hasilnya. Memahami pembagian peran kedua objek ini penting karena pola yang sama berlaku di semua database Python, bukan hanya SQLite. Pada chapter ini kita bedah keduanya sampai ke kebiasaan menutup koneksi dengan benar.

Peran Connection dan Cursor #

Connection adalah pintu ke database: objek ini dibuat lewat sqlite3.connect(), menangani komitmen data (transaksi), dan ditutup saat selesai. Cursor adalah utusan yang berangkat membawa satu perintah SQL lalu kembali membawa hasil:

ObjekTanggung jawabMethod khas
Connectionmembuka database, mengelola transaksi, menjaga umur sesicommit(), rollback(), close()
Cursormenjalankan SQL, memegang hasil, melacak posisi bacaexecute(), executemany(), fetchone()

Pemanggilan conn.execute(...) sebenarnya jalan pintas: connection secara diam-diam membuat cursor, menjalankan perintah, lalu mengembalikan cursor tersebut. Bentuk eksplisitnya tampak berikut:

python
import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, stok INTEGER)")
conn.executemany(
    "INSERT INTO produk (nama, stok) VALUES (?, ?)",
    [("Kopi Hitam 200g", 12), ("Teh Celup Isi 25", 30)],
)
conn.commit()
print(conn.execute("SELECT nama, stok FROM produk ORDER BY nama").fetchall())
conn.close()

Output yang Dihasilkan:

text
[('Kopi Hitam 200g', 12), ('Teh Celup Isi 25', 30)]

Method executemany() menjalankan perintah yang sama berulang kali untuk sekumpulan data — pasangan ("Kopi Hitam 200g", 12) dan ("Teh Celup Isi 25", 30) mengisi dua placeholder ? secara bergantian. Bentuk eksplisit cursor = conn.cursor() lalu cursor.execute(...) menghasilkan perilaku identik; kita bebas memilih, dan jalan pintas conn.execute() cukup untuk kebanyakan kasus.

Mengambil Hasil Secara Bertahap #

Cursor membaca hasil seperti kursor teks di editor: mulai dari atas, lalu maju setiap kali kita mengambil. Tiga method pengambilan tersedia:

MethodMengembalikanEfek pada posisi cursor
fetchone()satu baris berikutnya, atau None jika habismaju satu baris
fetchmany(n)list hingga n baris berikutnyamaju hingga n baris
fetchall()list seluruh baris yang tersisamaju sampai habis

Ketiganya berbagi satu cursor yang sama, sehingga pemanggilan berurutan melanjutkan posisi terakhir:

python
import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, stok INTEGER)")
conn.executemany(
    "INSERT INTO produk (nama, stok) VALUES (?, ?)",
    [
        ("Kopi Hitam 200g", 12),
        ("Teh Celup Isi 25", 30),
        ("Gula Pasir 1kg", 25),
        ("Beras Premium 5kg", 8),
        ("Susu UHT 1L", 15),
    ],
)
conn.commit()

cursor = conn.execute("SELECT nama, stok FROM produk ORDER BY id")
print("fetchone  :", cursor.fetchone())
print("fetchmany :", cursor.fetchmany(2))
print("fetchall  :", cursor.fetchall())
conn.close()

Output yang Dihasilkan:

text
fetchone  : ('Kopi Hitam 200g', 12)
fetchmany : [('Teh Celup Isi 25', 30), ('Gula Pasir 1kg', 25)]
fetchall  : [('Beras Premium 5kg', 8), ('Susu UHT 1L', 15)]

fetchone() mengambil Kopi, fetchmany(2) melanjutkan dari Teh dan Gula, dan fetchall() menyapu dua sisa baris. Untuk tabel berjuta baris, pengambilan bertahap seperti ini menjaga memori tetap ramping karena kita tidak pernah memuat seluruh hasil sekaligus.

Iterasi Cursor dan Penutupan yang Aman #

Cursor adalah iterator, jadi loop for membacanya baris demi baris tanpa fetchall():

python
import sqlite3

conn = sqlite3.connect(":memory:")
try:
    conn.execute("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, stok INTEGER)")
    conn.executemany(
        "INSERT INTO produk (nama, stok) VALUES (?, ?)",
        [("Kopi Hitam 200g", 12), ("Teh Celup Isi 25", 30), ("Gula Pasir 1kg", 25)],
    )
    conn.commit()
    for id_, nama, stok in conn.execute("SELECT id, nama, stok FROM produk"):
        print(f"#{id_} {nama} (stok {stok})")
finally:
    conn.close()

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

Output yang Dihasilkan:

text
#1 Kopi Hitam 200g (stok 12)
#2 Teh Celup Isi 25 (stok 30)
#3 Gula Pasir 1kg (stok 25)
Galat: Cannot operate on a closed database.

Blok try/finally menjamin conn.close() tercapai berapa pun jalannya program — termasuk saat exception terjadi di tengah. Percobaan conn.execute("SELECT 1") setelahnya membuktikan koneksi benar-benar sudah mati: interpreter Python menaikkan sqlite3.ProgrammingError. Koneksi yang dibiarkan terbuka menahan file database dan resource sistem, dan pada database server menahan slot koneksi yang mahal — kebiasaan menutup harus otomatis, seperti terlihat pada Chapter Connection Management.

Jebakan with conn #

Python menyediakan with conn: untuk connection — tetapi perilakunya mengejutkan pemakai with pada file:

python
import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE stok_log (id INTEGER PRIMARY KEY, catatan TEXT)")
conn.commit()

with conn:
    conn.execute("INSERT INTO stok_log (catatan) VALUES ('stok masuk gudang')")
    conn.execute("INSERT INTO stok_log (catatan) VALUES ('stok keluar toko')")

print("in_transaction:", conn.in_transaction)
print("Masih terbuka :", conn.execute("SELECT COUNT(*) FROM stok_log").fetchone()[0], "baris")

try:
    with conn:
        conn.execute("INSERT INTO stok_log (catatan) VALUES ('transaksi setengah jalan')")
        raise RuntimeError("aplikasi mogok sebelum transaksi selesai")
except RuntimeError as e:
    print("Galat:", e)

print("Baris tersimpan:", conn.execute("SELECT COUNT(*) FROM stok_log").fetchone()[0],
      "(insert ketiga dibatalkan)")
conn.close()

Output yang Dihasilkan:

text
in_transaction: False
Masih terbuka : 2 baris
Galat: aplikasi mogok sebelum transaksi selesai
Baris tersimpan: 2 (insert ketiga dibatalkan)

Blok with conn: mengakhiri transaksi: dua insert pertama di-commit saat blok selesai, dan insert ketiga yang diinterupsi RuntimeError di-rollback — buktinya jumlah baris tetap 2. Namun with pada connection tidak menutup koneksi: baris SELECT COUNT(*) setelah blok pertama masih berhasil dieksekusi. Kita tetap harus memanggil conn.close() sendiri.

Mekanisme commit dan rollback yang baru saja kita saksikan adalah pintu masuk materi Chapter Transactions, tempat jaminan atomicity dari Chapter Dasar Database Relasional akhirnya terasa manfaatnya.