Oracle #
Oracle Database adalah database relasional enterprise kelas atas yang dominan di industri perbankan, telekomunikasi, dan pemerintahan. Python berinteraksi dengan Oracle melalui library python-oracledb — penerus resmi dari cx_Oracle yang kini sudah deprecated. Salah satu keunikan Oracle dari database lain adalah penggunaan placeholder bernama (:nama bukan ? atau %s), konsep SEQUENCE untuk auto-increment, klausa RETURNING INTO, dan mode koneksi thin vs thick yang perlu dipahami sebelum memulai. Menguasai perbedaan ini akan menghindarkan kamu dari kebingungan saat beralih dari MySQL atau PostgreSQL ke Oracle.
Instalasi #
pip install python-oracledb
python-oracledb hadir dalam dua mode:
Mode Thin (default)
├── Pure Python, tidak perlu Oracle Client
├── Cukup pip install, langsung bisa digunakan
└── Mendukung sebagian besar fitur untuk aplikasi umum
Mode Thick
├── Membutuhkan Oracle Instant Client terinstal
├── Perlu diinisialisasi eksplisit: oracledb.init_oracle_client()
└── Mendukung fitur lanjutan: Advanced Queuing, DRCP, dll.
Untuk sebagian besar aplikasi baru, mode thin sudah cukup dan jauh lebih mudah di-setup. Gunakan mode thick hanya jika kamu membutuhkan fitur Oracle lanjutan yang tidak tersedia di mode thin, atau terhubung ke Oracle Database versi lama (sebelum 12.1).
Membuat Koneksi #
Oracle menggunakan DSN (Data Source Name) atau connection string yang berbeda dari database lain. Ada dua format umum: menggunakan Easy Connect String atau TNS alias.
import oracledb
import os
# ANTI-PATTERN: hardcode kredensial di kode
conn = oracledb.connect(
user="admin",
password="rahasia123", # ✗ -- jangan lakukan ini
dsn="localhost/ORCLPDB1"
)
# BENAR: baca dari environment variable
def get_connection() -> oracledb.Connection:
user = os.getenv("ORACLE_USER", "admin")
password = os.getenv("ORACLE_PASSWORD")
host = os.getenv("ORACLE_HOST", "localhost")
port = os.getenv("ORACLE_PORT", "1521")
service = os.getenv("ORACLE_SERVICE", "ORCLPDB1")
# Easy Connect String format: host:port/service_name
dsn = f"{host}:{port}/{service}"
return oracledb.connect(user=user, password=password, dsn=dsn)
conn = get_connection()
print("Koneksi berhasil, versi Oracle:", conn.version)
conn.close()
# Koneksi menggunakan TNS alias (dari tnsnames.ora)
conn = oracledb.connect(
user=os.getenv("ORACLE_USER"),
password=os.getenv("ORACLE_PASSWORD"),
dsn="MYDB_PROD" # alias yang didefinisikan di tnsnames.ora
)
# Mode thick (jika perlu fitur lanjutan)
oracledb.init_oracle_client(lib_dir="/opt/oracle/instantclient_21_9")
conn = oracledb.connect(user="admin", password="...", dsn="localhost/ORCLPDB1")
Context Manager untuk Koneksi #
import oracledb
from contextlib import contextmanager
import os
@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 SYSDATE FROM DUAL")
tanggal = cursor.fetchone()
print("Tanggal server Oracle:", tanggal[0])
Membuat Tabel dan Sequence #
Oracle tidak memiliki AUTO_INCREMENT — sebagai gantinya menggunakan SEQUENCE dan TRIGGER, atau sejak Oracle 12c menggunakan kolom GENERATED ALWAYS AS IDENTITY.
def create_tables(conn):
cursor = conn.cursor()
# Hapus tabel jika sudah ada (untuk dev/testing)
for obj in ["pengguna", "seq_pengguna"]:
try:
if obj.startswith("seq_"):
cursor.execute(f"DROP SEQUENCE {obj}")
else:
cursor.execute(f"DROP TABLE {obj} PURGE")
except oracledb.DatabaseError:
pass # abaikan jika belum ada
# Buat sequence untuk auto-increment (Oracle 11g dan sebelumnya)
cursor.execute("""
CREATE SEQUENCE seq_pengguna
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE
""")
# Buat tabel -- gunakan IDENTITY untuk Oracle 12c+
cursor.execute("""
CREATE TABLE pengguna (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama NVARCHAR2(100) NOT NULL,
email NVARCHAR2(150) NOT NULL,
usia NUMBER(3),
aktif NUMBER(1) DEFAULT 1,
dibuat_pada TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_pengguna_email UNIQUE (email)
)
""")
conn.commit()
cursor.close()
print("Tabel dan sequence berhasil dibuat.")
with db_connection() as conn:
create_tables(conn)
Perbedaan penting Oracle dari MySQL/MSSQL:
NUMBERmenggantikanINT,BIGINT,DECIMALNVARCHAR2untuk string Unicode (rekomendasi Oracle modern)TIMESTAMPmenggantikanDATETIMESYSDATEatauCURRENT_TIMESTAMPuntuk waktu serverDUAL— tabel dummy Oracle untuk query tanpa tabel nyata:SELECT 1 FROM DUAL- Tidak ada
BOOLEANnative — gunakanNUMBER(1)(0/1) atauCHAR(1)(Y/N)
Operasi CRUD #
Placeholder Bernama — Keunikan Oracle #
# ANTI-PATTERN: menggunakan ? (MSSQL) atau %s (MySQL)
cursor.execute("INSERT INTO pengguna (nama, email) VALUES (?, ?)", ("Budi", "[email protected]")) # ✗
cursor.execute("INSERT INTO pengguna (nama, email) VALUES (%s, %s)", ("Budi", "[email protected]")) # ✗
# BENAR: Oracle menggunakan placeholder bernama dengan awalan titik dua
cursor.execute(
"INSERT INTO pengguna (nama, email) VALUES (:nama, :email)",
{"nama": "Budi", "email": "[email protected]"} # ✓ -- dict dengan key sesuai placeholder
)
# Atau dengan tuple posisional (urutan harus sesuai)
cursor.execute(
"INSERT INTO pengguna (nama, email) VALUES (:1, :2)",
("Budi", "[email protected]") # ✓ -- :1, :2, :3, ...
)
Insert Data #
def tambah_pengguna(conn, nama: str, email: str, usia: int) -> int:
cursor = conn.cursor()
# RETURNING INTO untuk mendapatkan ID yang baru dibuat
id_var = cursor.var(oracledb.NUMBER)
cursor.execute(
"""
INSERT INTO pengguna (nama, email, usia)
VALUES (:nama, :email, :usia)
RETURNING id INTO :id_baru
""",
{"nama": nama, "email": email, "usia": usia, "id_baru": id_var}
)
id_baru = int(id_var.getvalue()[0])
cursor.close()
return id_baru
def tambah_banyak_pengguna(conn, daftar: list[dict]) -> int:
cursor = conn.cursor()
cursor.executemany(
"INSERT INTO pengguna (nama, email, usia) VALUES (:nama, :email, :usia)",
daftar
)
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 = [
{"nama": "Sari Dewi", "email": "[email protected]", "usia": 25},
{"nama": "Andi Prasetyo", "email": "[email protected]", "usia": 32},
{"nama": "Rina Marlina", "email": "[email protected]", "usia": 29},
]
jumlah = tambah_banyak_pengguna(conn, data_baru)
print(f"{jumlah} pengguna ditambahkan.")
Read Data #
def ambil_semua_pengguna(conn) -> list[dict]:
cursor = conn.cursor()
# rowfactory -- konversi baris menjadi dict otomatis
cursor.rowfactory = lambda *args: dict(zip(
[col[0].lower() for col in cursor.description], args
))
cursor.execute("""
SELECT id, nama, email, usia, aktif
FROM pengguna
WHERE aktif = 1
ORDER BY dibuat_pada DESC
FETCH FIRST 100 ROWS ONLY
""")
hasil = cursor.fetchall()
cursor.close()
return hasil
def ambil_pengguna_by_id(conn, pengguna_id: int) -> dict | None:
cursor = conn.cursor()
cursor.rowfactory = lambda *args: dict(zip(
[col[0].lower() for col in cursor.description], args
))
cursor.execute(
"SELECT id, nama, email, usia FROM pengguna WHERE id = :id",
{"id": pengguna_id}
)
hasil = cursor.fetchone()
cursor.close()
return hasil
# Pagination dengan OFFSET-FETCH (Oracle 12c+)
def ambil_pengguna_halaman(conn, halaman: int = 1, per_halaman: int = 10) -> list[dict]:
cursor = conn.cursor()
cursor.rowfactory = lambda *args: dict(zip(
[col[0].lower() for col in cursor.description], args
))
offset = (halaman - 1) * per_halaman
cursor.execute("""
SELECT id, nama, email, usia
FROM pengguna
ORDER BY id
OFFSET :offset ROWS
FETCH NEXT :per_halaman ROWS ONLY
""", {"offset": offset, "per_halaman": per_halaman})
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']}")
Update dan Delete #
def update_pengguna(conn, pengguna_id: int, nama: str, usia: int) -> bool:
cursor = conn.cursor()
cursor.execute(
"UPDATE pengguna SET nama = :nama, usia = :usia WHERE id = :id",
{"nama": nama, "usia": usia, "id": pengguna_id}
)
berhasil = cursor.rowcount > 0
cursor.close()
return berhasil
def hapus_pengguna(conn, pengguna_id: int) -> bool:
cursor = conn.cursor()
cursor.execute("DELETE FROM pengguna WHERE id = :id", {"id": pengguna_id})
berhasil = cursor.rowcount > 0
cursor.close()
return berhasil
Stored Procedure #
Oracle mendukung stored procedure dan function melalui PL/SQL. Pemanggilan dari Python menggunakan cursor.callproc() atau cursor.callfunc().
-- Buat stored procedure di Oracle terlebih dahulu
CREATE OR REPLACE PROCEDURE get_pengguna_by_email(
p_email IN pengguna.email%TYPE,
p_nama OUT pengguna.nama%TYPE,
p_usia OUT pengguna.usia%TYPE,
p_ditemukan OUT NUMBER
) AS
BEGIN
SELECT nama, usia INTO p_nama, p_usia
FROM pengguna
WHERE email = p_email AND ROWNUM = 1;
p_ditemukan := 1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_ditemukan := 0;
END;
import oracledb
def cari_pengguna_by_email(conn, email: str) -> dict | None:
cursor = conn.cursor()
# Siapkan variabel output
nama_var = cursor.var(oracledb.STRING)
usia_var = cursor.var(oracledb.NUMBER)
ditemukan_var = cursor.var(oracledb.NUMBER)
cursor.callproc(
"get_pengguna_by_email",
[email, nama_var, usia_var, ditemukan_var]
)
cursor.close()
if int(ditemukan_var.getvalue()) == 0:
return None
return {
"nama": nama_var.getvalue(),
"usia": int(usia_var.getvalue())
}
with db_connection() as conn:
pengguna = cari_pengguna_by_email(conn, "[email protected]")
if pengguna:
print(f"Ditemukan: {pengguna['nama']}, usia {pengguna['usia']}")
Transaksi #
def transfer_saldo(conn, dari_id: int, ke_id: int, jumlah: float) -> None:
cursor = conn.cursor()
try:
# Kunci baris untuk dibaca dan diupdate
cursor.execute(
"SELECT saldo FROM akun WHERE id = :id FOR UPDATE",
{"id": dari_id}
)
baris = cursor.fetchone()
if not baris or baris[0] < jumlah:
raise ValueError(f"Saldo tidak mencukupi. Tersedia: {baris[0] if baris else 0}")
cursor.execute(
"UPDATE akun SET saldo = saldo - :jumlah WHERE id = :id",
{"jumlah": jumlah, "id": dari_id}
)
cursor.execute(
"UPDATE akun SET saldo = saldo + :jumlah WHERE id = :id",
{"jumlah": jumlah, "id": ke_id}
)
conn.commit()
print(f"Transfer {jumlah:,.0f} berhasil.")
except Exception as e:
conn.rollback()
print(f"Transaksi dibatalkan: {e}")
raise
finally:
cursor.close()
Penanganan Error #
import oracledb
def tambah_pengguna_aman(conn, nama: str, email: str) -> int | None:
cursor = conn.cursor()
id_var = cursor.var(oracledb.NUMBER)
try:
cursor.execute(
"""
INSERT INTO pengguna (nama, email)
VALUES (:nama, :email)
RETURNING id INTO :id_baru
""",
{"nama": nama, "email": email, "id_baru": id_var}
)
conn.commit()
return int(id_var.getvalue()[0])
except oracledb.IntegrityError as e:
conn.rollback()
# ORA-00001: unique constraint violated
if "ORA-00001" in str(e):
print(f"Email '{email}' sudah terdaftar.")
else:
print(f"Integrity error: {e}")
return None
except oracledb.DatabaseError as e:
conn.rollback()
error_obj = e.args[0]
print(f"Oracle error [{error_obj.code}]: {error_obj.message}")
return None
finally:
cursor.close()
Ringkasan #
- Placeholder
:nama— Oracle menggunakan placeholder bernama (:nama) atau posisional (:1,:2); bukan?(MSSQL/pyodbc) atau%s(MySQL).RETURNING INTO— gunakan klausa ini untuk mendapatkan nilai kolom yang baru di-insert (termasuk ID), dikombinasikan dengancursor.var().cursor.rowfactory— set lambda ini agar setiap baris hasil query dikembalikan sebagaidictdengan nama kolom lowercase.GENERATED ALWAYS AS IDENTITY— cara modern Oracle 12c+ untuk auto-increment, menggantikan pola SEQUENCE + TRIGGER.NVARCHAR2bukanVARCHAR2— gunakan untuk kolom string yang menyimpan karakter non-ASCII/Unicode.FETCH FIRST N ROWS ONLY— padananLIMITdi Oracle 12c+; untuk Oracle 11g ke bawah gunakanROWNUM.cursor.callproc()/cursor.callfunc()— cara memanggil stored procedure dan function PL/SQL dari Python.- Mode thin vs thick — mode thin (default) tidak butuh Oracle Client dan cukup untuk sebagian besar kebutuhan; aktifkan mode thick hanya untuk fitur Oracle lanjutan.
ORA-00001— kode error untuk unique constraint violation; tangani secara spesifik diexcept oracledb.IntegrityError.