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:

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)"
)

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

text
(1, 'Budi', 'Jakarta')

Mari kita bedah bagian pentingnya:

  1. Baris 10 (... VALUES (?, ?)", ("Budi", "Jakarta")): dua tanda tanya diisi dua nilai dari tuple, urutannya harus sesuai.
  2. Baris 13-16 (... VALUES (:nama, :kota)", {...}): placeholder bernama diisi dari dictionary, jadi urutan penulisan tidak berpengaruh.
  3. Baris 18-22 (cur.execute(...).fetchone()): execute() mengembalikan cursor itu sendiri, sehingga fetchone() bisa dirantai langsung; dictionary kriteria dipakai 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:

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

text
lastrowid: 1
rowcount: 3
total baris: 4

Dua atribut cursor melaporkan efek statement terakhir:

AtributNilaiKeterangan
cur.lastrowid1rowid dari INSERT terakhir yang dijalankan lewat execute()
cur.rowcount3jumlah baris yang terpengaruh executemany()
cur.rowcount setelah SELECT-1SELECT 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:

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

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

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

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

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

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

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

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

Akar keduanya sama: parameter harus berupa sequence yang elemennya berisi satu nilai per placeholder.

  1. Kasus pertama: query memakai satu ?, tetapi tuple ("Budi", "Jakarta") membawa dua nilai — modul sqlite3 menolaknya karena jumlahnya tidak cocok.
  2. 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 elemennya str.

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.