MySQL #
MySQL adalah salah satu database relasional paling populer di dunia, digunakan secara luas mulai dari aplikasi web sederhana hingga sistem enterprise skala besar. Python menyediakan beberapa library untuk berinteraksi dengan MySQL, dan memahami cara menggunakannya dengan benar — termasuk parameterized query untuk mencegah SQL injection, manajemen transaksi, dan connection pooling — adalah fondasi penting dalam membangun aplikasi yang andal dan aman.
Instalasi #
Library resmi yang direkomendasikan untuk koneksi MySQL dari Python adalah mysql-connector-python yang dibuat oleh Oracle:
pip install mysql-connector-python
Alternatif populer lainnya adalah PyMySQL (pure Python, tanpa dependency C):
pip install pymysql
mysql-connector-pythonadalah pilihan yang lebih stabil untuk produksi karena didukung langsung oleh Oracle.PyMySQLlebih mudah diinstal di lingkungan yang tidak mendukung kompilasi C extension. Artikel ini menggunakanmysql-connector-python, tapi API dasarnya kompatibel dengan DB-API 2.0 yang diikuti keduanya.
Membuat Koneksi #
Koneksi ke MySQL membutuhkan beberapa parameter wajib: host, user, password, dan nama database. Jangan pernah menyimpan nilai-nilai ini langsung di kode — gunakan environment variable.
import mysql.connector
import os
# ANTI-PATTERN: hardcode kredensial di kode
conn = mysql.connector.connect(
host="localhost",
user="root",
password="password123", # ✗ -- jangan lakukan ini
database="myapp"
)
# BENAR: baca dari environment variable
conn = mysql.connector.connect(
host=os.getenv("DB_HOST", "localhost"),
port=int(os.getenv("DB_PORT", "3306")),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
database=os.getenv("DB_NAME"),
charset="utf8mb4", # dukung emoji dan karakter Unicode penuh
use_unicode=True,
autocommit=False # kelola transaksi secara eksplisit
)
print("Koneksi berhasil:", conn.is_connected())
conn.close()
Koneksi dengan Context Manager #
Cara yang lebih rapi adalah membungkus koneksi dalam fungsi dan menggunakan try/finally untuk memastikan koneksi selalu ditutup:
import mysql.connector
from contextlib import contextmanager
import os
def get_connection():
return mysql.connector.connect(
host=os.getenv("DB_HOST", "localhost"),
user=os.getenv("DB_USER", "root"),
password=os.getenv("DB_PASSWORD", ""),
database=os.getenv("DB_NAME", "myapp"),
charset="utf8mb4",
autocommit=False
)
@contextmanager
def db_connection():
conn = get_connection()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# Penggunaan
with db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT VERSION()")
versi = cursor.fetchone()
print("Versi MySQL:", versi[0])
Membuat Tabel #
Sebelum melakukan operasi data, buat tabel yang dibutuhkan. Gunakan IF NOT EXISTS agar script aman dijalankan berulang kali.
import mysql.connector
def create_tables(conn):
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS pengguna (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
usia INT,
aktif BOOLEAN DEFAULT TRUE,
dibuat_pada TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS produk (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
harga DECIMAL(15, 2) NOT NULL,
stok INT DEFAULT 0,
kategori VARCHAR(100)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.commit()
cursor.close()
print("Tabel berhasil dibuat.")
with db_connection() as conn:
create_tables(conn)
Operasi CRUD #
Insert Data #
import mysql.connector
def tambah_pengguna(conn, nama: str, email: str, usia: int) -> int:
cursor = conn.cursor()
# ANTI-PATTERN: string formatting langsung -- rentan SQL Injection
query = f"INSERT INTO pengguna (nama, email) VALUES ('{nama}', '{email}')" # ✗
# BENAR: gunakan parameterized query dengan placeholder %s
query = "INSERT INTO pengguna (nama, email, usia) VALUES (%s, %s, %s)"
cursor.execute(query, (nama, email, usia))
id_baru = cursor.lastrowid
cursor.close()
return id_baru
# Insert banyak baris sekaligus (lebih efisien)
def tambah_banyak_pengguna(conn, daftar_pengguna: list[tuple]) -> int:
cursor = conn.cursor()
query = "INSERT INTO pengguna (nama, email, usia) VALUES (%s, %s, %s)"
cursor.executemany(query, daftar_pengguna)
jumlah = cursor.rowcount
cursor.close()
return jumlah
with db_connection() as conn:
id1 = tambah_pengguna(conn, "Budi Santoso", "[email protected]", 28)
print(f"Pengguna baru ID: {id1}")
data_baru = [
("Sari Dewi", "[email protected]", 25),
("Andi Prasetyo", "[email protected]", 32),
("Rina Marlina", "[email protected]", 29),
]
jumlah = tambah_banyak_pengguna(conn, data_baru)
print(f"{jumlah} pengguna ditambahkan.")
SQL Injection adalah salah satu celah keamanan paling umum dan berbahaya. Jangan pernah membangun query SQL dengan string formatting atau concatenation menggunakan input dari pengguna. Selalu gunakan parameterized query dengan placeholder %s dan tuple nilai terpisah.Read Data #
def ambil_semua_pengguna(conn) -> list[dict]:
# dictionary=True -- row dikembalikan sebagai dict, bukan tuple
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM pengguna WHERE aktif = TRUE ORDER BY dibuat_pada DESC")
hasil = cursor.fetchall()
cursor.close()
return hasil
def ambil_pengguna_by_id(conn, pengguna_id: int) -> dict | None:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM pengguna WHERE id = %s", (pengguna_id,))
hasil = cursor.fetchone()
cursor.close()
return hasil
def cari_pengguna(conn, kata_kunci: str) -> list[dict]:
cursor = conn.cursor(dictionary=True)
# LIKE dengan parameterized query -- tambahkan % di nilai, bukan di query
cursor.execute(
"SELECT * FROM pengguna WHERE nama LIKE %s OR email LIKE %s",
(f"%{kata_kunci}%", f"%{kata_kunci}%")
)
hasil = cursor.fetchall()
cursor.close()
return hasil
with db_connection() as conn:
semua = ambil_semua_pengguna(conn)
for p in semua:
print(f"[{p['id']}] {p['nama']} — {p['email']}")
satu = ambil_pengguna_by_id(conn, 1)
if satu:
print(f"Ditemukan: {satu['nama']}")
hasil_cari = cari_pengguna(conn, "budi")
print(f"Hasil pencarian: {len(hasil_cari)} pengguna")
Update Data #
def update_pengguna(conn, pengguna_id: int, nama: str, usia: int) -> bool:
cursor = conn.cursor()
query = "UPDATE pengguna SET nama = %s, usia = %s WHERE id = %s"
cursor.execute(query, (nama, usia, pengguna_id))
berhasil = cursor.rowcount > 0
cursor.close()
return berhasil
def nonaktifkan_pengguna(conn, pengguna_id: int) -> bool:
cursor = conn.cursor()
cursor.execute("UPDATE pengguna SET aktif = FALSE WHERE id = %s", (pengguna_id,))
berhasil = cursor.rowcount > 0
cursor.close()
return berhasil
with db_connection() as conn:
ok = update_pengguna(conn, 1, "Budi Santoso Wijaya", 29)
print("Update berhasil:", ok)
Delete Data #
def hapus_pengguna(conn, pengguna_id: int) -> bool:
cursor = conn.cursor()
cursor.execute("DELETE FROM pengguna WHERE id = %s", (pengguna_id,))
berhasil = cursor.rowcount > 0
cursor.close()
return berhasil
with db_connection() as conn:
ok = hapus_pengguna(conn, 99)
print("Hapus berhasil:", ok)
Transaksi #
Transaksi memastikan serangkaian operasi database dieksekusi secara atomik — semua berhasil atau semua dibatalkan. Ini krusial untuk operasi yang melibatkan beberapa tabel atau beberapa baris yang harus konsisten.
def transfer_stok(conn, dari_produk_id: int, ke_produk_id: int, jumlah: int) -> None:
cursor = conn.cursor()
try:
# Cek stok tersedia
cursor.execute("SELECT stok FROM produk WHERE id = %s FOR UPDATE", (dari_produk_id,))
baris = cursor.fetchone()
if not baris or baris[0] < jumlah:
raise ValueError(f"Stok tidak mencukupi. Tersedia: {baris[0] if baris else 0}")
# Kurangi stok sumber
cursor.execute(
"UPDATE produk SET stok = stok - %s WHERE id = %s",
(jumlah, dari_produk_id)
)
# Tambah stok tujuan
cursor.execute(
"UPDATE produk SET stok = stok + %s WHERE id = %s",
(jumlah, ke_produk_id)
)
conn.commit()
print(f"Transfer {jumlah} unit berhasil.")
except Exception as e:
conn.rollback()
print(f"Transaksi dibatalkan: {e}")
raise
finally:
cursor.close()
Connection Pool #
Membuat koneksi baru ke database setiap request adalah operasi yang mahal. Connection pool mempertahankan sekumpulan koneksi yang siap pakai dan mendaur ulangnya.
import mysql.connector
from mysql.connector import pooling
import os
# Buat pool saat aplikasi start
connection_pool = pooling.MySQLConnectionPool(
pool_name="myapp_pool",
pool_size=5, # jumlah koneksi yang dipertahankan
pool_reset_session=True,
host=os.getenv("DB_HOST", "localhost"),
user=os.getenv("DB_USER", "root"),
password=os.getenv("DB_PASSWORD", ""),
database=os.getenv("DB_NAME", "myapp"),
charset="utf8mb4"
)
def get_pooled_connection():
return connection_pool.get_connection()
# Penggunaan -- koneksi otomatis dikembalikan ke pool saat close()
def ambil_pengguna_pool(pengguna_id: int) -> dict | None:
conn = get_pooled_connection()
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM pengguna WHERE id = %s", (pengguna_id,))
return cursor.fetchone()
finally:
conn.close() # dikembalikan ke pool, bukan benar-benar ditutup
Penanganan Error #
import mysql.connector
from mysql.connector import Error, errorcode
def tambah_pengguna_aman(conn, nama: str, email: str) -> int | None:
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO pengguna (nama, email) VALUES (%s, %s)",
(nama, email)
)
conn.commit()
return cursor.lastrowid
except Error as e:
conn.rollback()
if e.errno == errorcode.ER_DUP_ENTRY:
print(f"Email '{email}' sudah terdaftar.")
elif e.errno == errorcode.ER_BAD_NULL_ERROR:
print("Ada field wajib yang kosong.")
else:
print(f"Error database [{e.errno}]: {e.msg}")
return None
finally:
cursor.close()
Ringkasan #
- Parameterized query wajib — selalu gunakan
%ssebagai placeholder dan tuple nilai terpisah; jangan pernah format string SQL langsung dari input pengguna.cursor(dictionary=True)— gunakan agar baris hasil query dikembalikan sebagaidict(akses via nama kolom) bukantuple(akses via indeks).charset="utf8mb4"— gunakan charset ini (bukanutf8) agar mendukung emoji dan seluruh karakter Unicode dengan benar.- Jangan hardcode kredensial — baca host, user, password dari environment variable, bukan langsung di kode.
commit()danrollback()eksplisit — aturautocommit=Falsedan kelola transaksi secara manual untuk kontrol penuh atas atomicity.executemany()— gunakan untuk insert banyak baris sekaligus; jauh lebih efisien daripada loopexecute()satu per satu.- Connection pool — gunakan
MySQLConnectionPooldi aplikasi web/server agar tidak membuat koneksi baru setiap request.cursor.lastrowid— ambil ID baris yang baru saja di-insert tanpa perlu query tambahan.cursor.rowcount— cek apakah UPDATE atau DELETE benar-benar mengubah baris, bukan hanya berjalan tanpa efek.