Aplikasi yang sehat bukan aplikasi yang tidak pernah mendapat error database — aplikasi seperti itu tidak ada. Server bisa mati, password bisa berganti saat rotasi, dua pengguna bisa memasukkan email yang sama pada detik yang sama, dan dua transaksi bisa saling mengunci diam-diam. Yang memisahkan aplikasi sehat dari yang rapuh adalah apa yang terjadi setelah error tiba: apakah pengguna melihat pesan yang membingungkan (atau halaman kosong), ataukah pesan yang jujur dan tetap sopan; apakah detail teknisnya hilang begitu, ataukah tercatat lengkap untuk yang memperbaiki. Chapter penutup section ini memetakan error MySQL yang paling sering dijumpai aplikasi, lalu menangani tiga di antaranya secara nyata lewat PyMySQL — termasuk satu pola yang wajib dihafal: deadlock itu bukan kegagalan, melainkan perintah untuk mencoba lagi.
Kode Error dan SQLSTATE #
Setiap error MySQL membawa dua identitas. Error code — angka empat digit seperti 1062 — spesifik milik MySQL dan sangat detail: dua pelanggaran constraint yang berbeda (duplikat kunci versus foreign key gagal) mendapat kode berbeda. SQLSTATE — kode lima karakter seperti 23000 — mengikuti standar SQL lintas-database dan lebih kasar: semua pelanggaran integritas data berbagi kelas 23, kelas 40 berarti rollback berseri (serialisasi, termasuk deadlock), kelas 42 berarti kesalahan sintaks atau nama. Klien mysql menampilkan keduanya berdampingan — ERROR 1062 (23000) — dan driver aplikasi kita membawa keduanya juga: PyMySQL menaruh error code di err.args[0].
Kapan memakai yang mana? Kode MySQL (1062) tepat untuk keputusan penanganan yang spesifik — “baris sudah ada, tampilkan pesan”. SQLSTATE tepat untuk penanganan per-kelas yang portabel — “semua error integritas, batalkan form” — terutama bila suatu saat database diganti dan kode MySQL tidak berlaku lagi. Keduanya selalu tersedia; pilihan bergantung pada seberapa spesifik reaksi kita.
Peta Error Umum dan Tindakannya #
| Kode | SQLSTATE | Wujud | Penyebab umum | Tindakan aplikasi |
|---|---|---|---|---|
| 1045 | 28000 | Access denied | user/password salah, host tidak diizinkan | periksa kredensial dan hak akses |
| 2003 | HY000 | Can’t connect to MySQL server | server mati, port salah, firewall | cek status server, host, port; tampilkan mode terdegradasi |
| 1049 | 42000 | Unknown database | nama database salah atau belum dibuat | periksa nama; jalankan migration bila environment baru |
| 1062 | 23000 | Duplicate entry | PRIMARY atau UNIQUE bentrok | pesan ramah (“sudah terdaftar”) atau ON DUPLICATE KEY UPDATE |
| 1452 | 23000 | Cannot add or update a child row | foreign key: induk belum ada | validasi relasi; masukkan induk dulu |
| 1451 | 23000 | Cannot delete or update a parent row | foreign key: baris masih dipakai anak | tolak hapus dengan pesan jelas, atau ON DELETE CASCADE |
| 1213 | 40001 | Deadlock found | dua transaksi saling menunggu kunci | ulangi transaksi dari awal — aman dan lazim |
| 1205 | HY000 | Lock wait timeout | menunggu kunci terlalu lama | perpendek transaksi; periksa query lambat |
| 2006 | HY000 | MySQL server has gone away | koneksi idle diputus server | reconnect atau pool dengan health check |
Tiga baris pertama adalah error koneksi — aplikasi bahkan belum sempat bicara SQL; sisanya error statement — koneksi sehat, satu perintah tertolak. beda keduanya menentukan letak penanganan: error koneksi ditangkap di sekitar connect(), error statement di sekitar execute().
Menyiapkan Data Latihan #
Skema kita memakai tiga tabel dengan dua foreign key — cukup untuk memicu error duplikat dan error relasi sekaligus:
DROP DATABASE IF EXISTS belajar_app;
CREATE DATABASE belajar_app;
USE belajar_app;
CREATE TABLE produk (
id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
harga INT NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Kopi Toraja', 52000),
(2, 'Teh Melati', 28000);
CREATE TABLE pelanggan (
id INT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE
);
INSERT INTO pelanggan (id, nama, email) VALUES
(1, 'Budi Santoso', '[email protected]');
CREATE TABLE pesanan (
id INT PRIMARY KEY,
pelanggan_id INT NOT NULL,
produk_id INT NOT NULL,
jumlah INT NOT NULL,
FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id),
FOREIGN KEY (produk_id) REFERENCES produk(id)
);
INSERT INTO pesanan (id, pelanggan_id, produk_id, jumlah) VALUES
(1, 1, 1, 2);Memicu Error dengan Sengaja #
Tiga error paling produktif untuk dipicu di klien. Yang pertama: memasukkan produk dengan id yang sudah terpakai — pelanggaran PRIMARY key:
-- @error
INSERT INTO produk (id, nama, harga) VALUES (1, 'Kopi Palsu', 99000);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1062 (23000): Duplicate entry '1' for key 'produk.PRIMARY'
ByePerhatikan formatnya: ERROR 1062 (23000) — error code di depan, SQLSTATE di kurung — persis pasangan yang barusan kita bedah. Yang kedua: membuat pesanan untuk pelanggan bernomor 99 yang tidak pernah ada — foreign key menolak anak yatim:
-- @error
INSERT INTO pesanan (id, pelanggan_id, produk_id, jumlah) VALUES (2, 99, 1, 1);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`belajar_app`.`pesanan`, CONSTRAINT `pesanan_ibfk_1` FOREIGN KEY (`pelanggan_id`) REFERENCES `pelanggan` (`id`))
ByePesan errornya panjang karena informatif: MySQL menyebut tabel anak, nama constraint, dan tabel induk yang gagal dipenuhi — cukup detail untuk mendiagnosis tanpa menebak. Kode 1452 dan 1062 sama-sama ber-SQLSTATE 23000; itulah contoh nyata “kode MySQL detail, SQLSTATE kasar”. Yang ketiga: menyebut database yang tidak ada — error koneksi-jenis yang akan kita jumpai di aplikasi:
-- @error
USE database_tidak_ada;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1049 (42000): Unknown database 'database_tidak_ada'
ByeKetiga error di atas sengaja dipicu di sesi klien agar kita melihat wujud aslinya. Di aplikasi, wujud yang sama hadir sebagai exception — dan di situlah penanganan sesungguhnya terjadi.
Menangani dari Aplikasi: tangani.py #
Berikut tangani.py — tersimpan di bundle chapter (bagian Berkas Sumber). Program ini memicu tiga error secara terkendali: INSERT duplikat di belajar_app, koneksi ke database yang tidak ada, dan koneksi ke port yang tidak difasilitasi server:
"""tangani.py — menangani error MySQL dari sisi aplikasi dengan PyMySQL.
Jalankan dari folder chapter: python3 tangani.py
Port default 3306; timpa lewat variabel lingkungan DB_PORT.
"""
import os
import pymysql
port = int(os.environ.get("DB_PORT", "3306"))
print("== 1062: INSERT duplikat ==")
with pymysql.connect(
host="127.0.0.1",
user="root",
password="",
database="belajar_app",
port=port,
) as koneksi:
try:
with koneksi.cursor() as kursor:
kursor.execute(
"INSERT INTO produk (id, nama, harga) VALUES (%s, %s, %s)",
(1, "Kopi Duplikat", 99000),
)
koneksi.commit()
print("Produk tersimpan.")
except pymysql.err.IntegrityError as err:
if err.args[0] == 1062:
print("Produk dengan id 1 sudah ada; data lama tetap dipakai.")
else:
print("Penyimpanan gagal, kode error:", err.args[0])
print("== 1049: database tidak ada ==")
try:
pymysql.connect(
host="127.0.0.1",
user="root",
password="",
database="database_tidak_ada",
port=port,
)
except pymysql.err.OperationalError as err:
print("Koneksi ditolak kode 1049: database tidak ditemukan, cek namanya.")
print("== 2003: server tidak merespons ==")
try:
pymysql.connect(
host="127.0.0.1",
user="root",
password="",
database="belajar_app",
port=3307,
connect_timeout=2,
)
except pymysql.err.OperationalError as err:
print("Koneksi gagal kode 2003: server tidak terjangkau di port itu.")Menjalankannya dari terminal (dengan DB_PORT seperti biasa; di komputer Anda cukup python3 tangani.py):
-- @test-bash
DB_PORT=3306 python3 content/mysql/integrasi-aplikasi/menangani-database-errors/tangani.pyOutput yang Dihasilkan:
== 1062: INSERT duplikat ==
Produk dengan id 1 sudah ada; data lama tetap dipakai.
== 1049: database tidak ada ==
Koneksi ditolak kode 1049: database tidak ditemukan, cek namanya.
== 2003: server tidak merespons ==
Koneksi gagal kode 2003: server tidak terjangkau di port itu.Bedah tiga polanya satu per satu:
- Blok pertama (1062):
INSERTduplikat ditangkap sebagaipymysql.err.IntegrityError, lalu kode dierr.args[0]diperiksa sebelum memutuskan reaksi. Kode1062mendapat pesan ramah yang sesuai konteks bisnis (“produk sudah ada”) — bukan teks mentah dari server. Cabangelsetetap ada: error integritas lain (misalnya1452) tidak boleh menyamar sebagai pesan “sudah ada”. Perhatikan juga letakkoneksi.commit(): hanya tercapai bila INSERT sukses — kegagalan membuat bloktrylompat langsung keexcept. - Blok kedua (1049): koneksi ke database fiktif ditolak sebelum sesi terbentuk, ditangkap sebagai
OperationalError. Dua kata di pesan kunci: cek namanya — pesan yang menyuruh orang memeriksa sesuatu yang bisa diperiksa, jauh lebih berguna daripada nomor kode mentah. - Blok ketiga (2003): koneksi ke port
3307(tidak ada server di sana) gagal setelahconnect_timeout=2detik — parameter yang memastikan aplikasi tidak menggantung menunggu server yang tidak akan menjawab.err.args[0]bernilai2003, pola yang sama dengan server benar-benar mati di produksi.
PyMySQL memetakan kode ke kelas exception: IntegrityError untuk pelanggaran constraint (kelas SQLSTATE 23), OperationalError untuk kegagalan operasional seperti koneksi, ProgrammingError untuk SQL salah, DataError untuk nilai tak cocok. Memilih kelas yang tepat di except adalah setengah dari penanganan yang bersih — setengah sisanya adalah reaksi yang sesuai kode.
Deadlock: Error yang Diminta untuk Diulang #
Satu kode di tabel kita menuntut reaksi berbeda dari semuanya. Deadlock (1213) terjadi ketika dua transaksi masing-masing memegang kunci yang dibutuhkan transaksi lain; InnoDB mendeteksi siklus itu, membatalkan salah satu transaksi (si “korban”), dan mengembalikan kontrol ke aplikasi — transaksi pemenang berjalan normal. Data tidak rusak, konsistensi tetap terjaga; satu-satunya tindakan yang benar bagi korban adalah mengulangi seluruh transaksi dari awal, karena rollback sudah dilakukan server.
Pola retry-nya lazim ditulis dengan jeda berlipat (backoff) agar serentakan request tidak mengulang tabrakan yang sama:
import time
for percobaan in range(3):
try:
with koneksi.cursor() as kursor:
kursor.execute("UPDATE produk SET stok = stok - %s WHERE id = %s", (1, 1))
koneksi.commit()
break
except pymysql.err.OperationalError as err:
if err.args[0] == 1213 and percobaan < 2:
time.sleep(0.05 * 2**percobaan)
continue
raisePerulangan mencoba hingga tiga kali: break keluar saat transaksi berhasil, time.sleep memberi jeda yang membesar (0,05 detik, lalu 0,1) sebelum mencoba lagi, dan raise melempar error yang bukan deadlock — atau deadlock yang bertahan setelah usaha terakhir — ke penangan di atasnya. Dua aturan yang menyertainya: ulangi transaksi lengkap (bukan statement terakhir saja, sebab sebagian perubahan sudah di-rollback), dan pasang batas percobaan — deadlock yang terus terjadi adalah gejala desain transaksi yang terlalu panjang atau urutan akses yang tidak konsisten, bukan masalah yang diselesaikan retry tanpa batas.
Pesan untuk Pengguna, Detail untuk Log #
Aturan terakhir section ini sependek pasalnya dan sebesar konsekuensinya: pesan mentah database tidak boleh sampai ke pengguna. Teks error MySQL memuat nama tabel, nama constraint, dan potongan nilai data — peta gratis bagi penyerang yang sedang memetasi struktur kita (persis materi SQL injection di section keamanan). Pola yang sehat bercabang dua: pengguna menerima kalimat pendek yang bisa ditindaklanjuti (“email ini sudah terdaftar, silakan masuk”), sementara baris lengkapnya — kode, SQLSTATE, query bila perlu — masuk log server bersama konteks: request apa, user mana, timestamp. Log inilah yang kita baca saat reproduksi bug: error yang tertangkap lalu dilupakan tanpa dicatat adalah bug yang akan kembali tanpa jejak.
Membersihkan Latihan #
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root -e "DROP DATABASE IF EXISTS belajar_app;"Referensi #
- Error Handling — MySQL 8.4 Reference Manual
- Deadlocks in InnoDB — MySQL 8.4 Reference Manual
- Deadlock Detection — MySQL 8.4 Reference Manual
Dengan itu, tugas paling berat integrasi aplikasi tuntas: koneksi dibuka hemat lewat pooling, query dikirim aman lewat placeholder, skema berubah terkendali lewat migration, data awal hadir lewat seeding, dan kegagalan disambut dengan reaksi yang tepat. Section berikutnya naik kelas dari memakai SQL ke mendalami SQL — Advanced SQL membahas teknik-teknik yang selama ini kita lewati demi kejelasan alur.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
tangani.py
Unduh berkas"""tangani.py — menangani error MySQL dari sisi aplikasi dengan PyMySQL.
Jalankan dari folder chapter: python3 tangani.py
Port default 3306; timpa lewat variabel lingkungan DB_PORT.
"""
import os
import pymysql
port = int(os.environ.get("DB_PORT", "3306"))
print("== 1062: INSERT duplikat ==")
with pymysql.connect(
host="127.0.0.1",
user="root",
password="",
database="belajar_app",
port=port,
) as koneksi:
try:
with koneksi.cursor() as kursor:
kursor.execute(
"INSERT INTO produk (id, nama, harga) VALUES (%s, %s, %s)",
(1, "Kopi Duplikat", 99000),
)
koneksi.commit()
print("Produk tersimpan.")
except pymysql.err.IntegrityError as err:
if err.args[0] == 1062:
print("Produk dengan id 1 sudah ada; data lama tetap dipakai.")
else:
print("Penyimpanan gagal, kode error:", err.args[0])
print("== 1049: database tidak ada ==")
try:
pymysql.connect(
host="127.0.0.1",
user="root",
password="",
database="database_tidak_ada",
port=port,
)
except pymysql.err.OperationalError as err:
print("Koneksi ditolak kode 1049: database tidak ditemukan, cek namanya.")
print("== 2003: server tidak merespons ==")
try:
pymysql.connect(
host="127.0.0.1",
user="root",
password="",
database="belajar_app",
port=3307,
connect_timeout=2,
)
except pymysql.err.OperationalError as err:
print("Koneksi gagal kode 2003: server tidak terjangkau di port itu.")