Membuat query dengan merangkai string — "SELECT * FROM pelanggan WHERE nama = '" + nama + "'" — berbahaya sekaligus rapuh: nilai yang mengandung tanda kutip bisa merusak query, dan input jahat bisa mengubah makna SQL sepenuhnya. Serangan ini bernama SQL injection dan dibedah tuntas pada Chapter Melindungi dari SQL Injection. Solusinya adalah query berparameter: struktur SQL dan data dikirim terpisah, lalu engine SQLite yang menggabungkannya dengan aman.
Placeholder ? dan :nama #
Modul sqlite3 mendukung dua gaya placeholder. Gaya qmark memakai tanda tanya ? dan tuple berisi nilai sesuai urutan; gaya named memakai :nama dan dictionary:
import sqlite3
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute(
"CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL, kota TEXT)"
)
# Parameter posisional dengan tanda tanya
cur.execute("INSERT INTO pelanggan (nama, kota) VALUES (?, ?)", ("Budi", "Jakarta"))
# Parameter bernama dengan dictionary
cur.execute(
"INSERT INTO pelanggan (nama, kota) VALUES (:nama, :kota)",
{"nama": "Siti", "kota": "Bandung"},
)
kriteria = {"kota": "Jakarta", "nama": "Budi"}
baris = cur.execute(
"SELECT id, nama, kota FROM pelanggan WHERE kota = :kota AND nama = :nama",
kriteria,
).fetchone()
print(baris)
con.close()Output yang Dihasilkan:
(1, 'Budi', 'Jakarta')Mari kita bedah bagian pentingnya:
- Baris 10 (
... VALUES (?, ?)", ("Budi", "Jakarta")): dua tanda tanya diisi dua nilai dari tuple, urutannya harus sesuai. - Baris 13-16 (
... VALUES (:nama, :kota)", {...}): placeholder bernama diisi dari dictionary, jadi urutan penulisan tidak berpengaruh. - Baris 18-22 (
cur.execute(...).fetchone()):execute()mengembalikan cursor itu sendiri, sehinggafetchone()bisa dirantai langsung; dictionarykriteriadipakai ulang untuk query berparameter bernama.
Nilai yang didukung placeholder mencakup int, float, str, bytes, dan None. Nilai None disimpan sebagai NULL — kuncinya, None tetap dikirim sebagai parameter, bukan ditulis langsung ke string SQL.
executemany, lastrowid, dan rowcount #
Untuk menyisipkan banyak baris, executemany() menerima daftar berisi parameter per baris dan menjalankan statement yang sama berulang kali — jauh lebih rapi daripada loop execute() manual:
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.execute("INSERT INTO pelanggan (nama, kota) VALUES (?, ?)", ("Budi", "Jakarta"))
print("lastrowid:", cur.lastrowid)
data = [
("Siti", "Bandung"),
("Ani", "Surabaya"),
("Rina", "Medan"),
]
cur.executemany("INSERT INTO pelanggan (nama, kota) VALUES (?, ?)", data)
print("rowcount:", cur.rowcount)
print("total baris:", cur.execute("SELECT COUNT(*) FROM pelanggan").fetchone()[0])
con.close()Output yang Dihasilkan:
lastrowid: 1
rowcount: 3
total baris: 4Dua atribut cursor melaporkan efek statement terakhir:
| Atribut | Nilai | Keterangan |
|---|---|---|
cur.lastrowid | 1 | rowid dari INSERT terakhir yang dijalankan lewat execute() |
cur.rowcount | 3 | jumlah baris yang terpengaruh executemany() |
cur.rowcount setelah SELECT | -1 | SELECT tidak mengubah baris, jadi tidak dihitung |
Satu kejutan kecil: pada Python 3.12, executemany() tidak memperbarui lastrowid — nilainya tetap dari statement execute() sebelumnya (di atas tetap 1, padahal baris terakhir yang disisipkan bernomor 4). Jika butuh id baris yang baru disisipkan, lakukan satu execute() INSERT dan baca lastrowid-nya.
None, NULL, dan Tanggal #
Python None menjadi NULL di database — berguna untuk kolom opsional. Membacanya kembali menghasilkan None juga:
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.execute("INSERT INTO pelanggan (nama, kota) VALUES (?, ?)", ("Tanpa Kota", None))
print(cur.execute(
"SELECT nama, kota FROM pelanggan WHERE nama = ?", ("Tanpa Kota",)
).fetchone())
print(cur.execute(
"SELECT COUNT(*) FROM pelanggan WHERE kota IS NULL"
).fetchone())
con.close()Output yang Dihasilkan:
('Tanpa Kota', None)
(1,)Untuk tanggal dan waktu, SQLite tidak punya tipe khusus — konvensinya menyimpan teks ISO YYYY-MM-DD HH:MM:SS yang dimengerti fungsi tanggal SQLite (materi Chapter Bekerja dengan Tanggal dan Waktu). Objek datetime dulu bisa dikirim langsung, tetapi adapter bawaannya kini usang:
python3 -c "
from datetime import datetime
import sqlite3
con = sqlite3.connect(':memory:')
con.execute('CREATE TABLE pesanan (id INTEGER PRIMARY KEY, tanggal TEXT)')
con.execute('INSERT INTO pesanan (tanggal) VALUES (?)', (datetime(2025, 6, 15, 10, 30),))
print(con.execute('SELECT tanggal FROM pesanan').fetchone())"Output yang Dihasilkan:
<string>:6: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes
('2025-06-15 10:30:00',)Konversi masih berjalan di Python 3.12 (hasilnya 2025-06-15 10:30:00), tetapi warning tersebut menandakan adapter ini akan dihapus. Praktik yang dianjurkan: format sendiri menjadi string ISO sebelum dikirim:
from datetime import datetime
import sqlite3
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, tanggal TEXT NOT NULL)")
waktu = datetime(2025, 6, 15, 10, 30)
cur.execute(
"INSERT INTO pesanan (tanggal) VALUES (?)",
(waktu.isoformat(sep=" ", timespec="seconds"),),
)
print(cur.execute("SELECT tanggal FROM pesanan").fetchone())
con.close()Output yang Dihasilkan:
('2025-06-15 10:30:00',)Metode isoformat(sep=" ", timespec="seconds") menghasilkan 2025-06-15 10:30:00 — persis format yang dikenali fungsi date(), strftime(), dan kawan-kawannya di sisi SQL.
Jebakan Tuple Bersarang #
Dua galat paling umum saat mulai memakai placeholder adalah jumlah parameter yang tidak cocok dan tipe nilai yang tidak didukung. Keduanya muncul sebagai sqlite3.ProgrammingError:
import sqlite3
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE pelanggan (id INTEGER PRIMARY KEY, nama TEXT NOT NULL)")
try:
cur.execute("INSERT INTO pelanggan (nama) VALUES (?)", ("Budi", "Jakarta"))
except sqlite3.ProgrammingError as e:
print("jumlah salah:", e)
try:
cur.execute("INSERT INTO pelanggan (nama) VALUES (?)", (("Budi",),))
except sqlite3.ProgrammingError as e:
print("tipe salah :", e)
con.close()Output yang Dihasilkan:
jumlah salah: Incorrect number of bindings supplied. The current statement uses 1, and there are 2 supplied.
tipe salah : Error binding parameter 1: type 'tuple' is not supportedAkar keduanya sama: parameter harus berupa sequence yang elemennya berisi satu nilai per placeholder.
- Kasus pertama: query memakai satu
?, tetapi tuple("Budi", "Jakarta")membawa dua nilai — modulsqlite3menolaknya karena jumlahnya tidak cocok. - Kasus kedua: tuple
(("Budi",),)memang berisi satu elemen, tetapi elemennya adalah tuple lain, yaitu("Budi",)— dan SQLite tidak punya cara menyimpan tuple ke dalam kolom. Pola yang benar untuk satu parameter string adalah("Budi",): satu elemen, koma di akhir, tipe elemennyastr.
Query berparameter mengamankan data masuk. Topik berikutnya mengamankan data tersimpan: bagaimana modul sqlite3 membuka transaction di belakang layar, dan bagaimana kita mengendalikan commit serta rollback.