Oracle #
Oracle Database adalah sistem manajemen database enterprise paling matang di dunia — digunakan oleh bank, perusahaan telekomunikasi, pemerintahan, dan organisasi besar yang membutuhkan keandalan tinggi, fitur enterprise seperti RAC (Real Application Clusters), Flashback, dan Advanced Compression. PHP mengakses Oracle melalui dua jalur: OCI8 (ekstensi khusus Oracle yang kaya fitur) dan PDO_OCI (driver PDO untuk Oracle). OCI8 lebih direkomendasikan untuk Oracle karena mendukung fitur-fitur khas Oracle seperti cursor, LOB, array binding, dan koneksi ke Oracle Cloud secara penuh — sesuatu yang tidak bisa dilakukan PDO_OCI dengan sempurna. Artikel ini membahas keduanya, dengan penekanan pada fitur-fitur Oracle yang tidak ada di database lain.
OCI8 vs PDO_OCI #
flowchart TD
PHP[PHP Application] --> A[OCI8\nDirekomendasikan untuk Oracle]
PHP --> B[PDO_OCI\nPortabilitas kode]
A --> C[Oracle Call Interface\nOCI Library]
B --> C
C --> D[(Oracle Database\nOn-premise / Cloud)]
style A fill:#dcfce7,stroke:#16a34a
style C fill:#dbeafe| Aspek | OCI8 | PDO_OCI |
|---|---|---|
| Dukungan fitur Oracle | ✓ Penuh | Terbatas |
| Cursor / REF CURSOR | ✓ | ✗ |
| LOB (CLOB, BLOB) | ✓ Streaming | Terbatas |
| Array binding | ✓ | ✗ |
| Named parameter | :nama | :nama |
| Connection pooling | ✓ DRCP bawaan | Manual |
| Portabilitas kode | ✗ Oracle only | ✓ |
Gunakan OCI8 untuk aplikasi yang serius dengan Oracle — fitur-fitur penting seperti cursor dan LOB handling tidak tersedia lengkap di PDO_OCI. Gunakan PDO_OCI hanya jika portabilitas kode antara Oracle dan database lain adalah prioritas.
Instalasi OCI8 #
Prasyarat: Oracle Instant Client #
OCI8 membutuhkan Oracle Instant Client — library koneksi Oracle yang harus diinstal terpisah:
# Linux — Download dari: https://www.oracle.com/database/technologies/instant-client/downloads.html
# Pilih: instantclient-basiclite-linux.x64-21.x.x.x.zip
# Ekstrak ke direktori
sudo mkdir -p /opt/oracle
sudo unzip instantclient-basiclite-linux.x64-21.*.zip -d /opt/oracle/
# Set library path
echo /opt/oracle/instantclient_21_x | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf
sudo ldconfig
# Install ekstensi OCI8 via PECL
sudo apt-get install php8.3-dev php-pear build-essential libaio1
sudo pecl install oci8
# Saat ditanya path Instant Client:
# instantclient,/opt/oracle/instantclient_21_x
# Aktifkan ekstensi
echo "extension=oci8.so" | sudo tee /etc/php/8.3/mods-available/oci8.ini
sudo phpenmod oci8
# Verifikasi
php -m | grep oci8
Koneksi ke Oracle #
Oracle memiliki beberapa cara mendefinisikan target koneksi:
<?php
// 1. Easy Connect — cara paling sederhana
// Format: //host[:port]/service_name
$koneksi = oci_connect('username', 'password', '//localhost:1521/ORCL');
// 2. Easy Connect dengan opsi lengkap
$koneksi = oci_connect(
username: 'hr',
password: 'welcome1',
connection_string: '//oracle-server.example.com:1521/XEPDB1',
character_set: 'AL32UTF8', // UTF-8 untuk Unicode
);
// 3. TNS (Transparent Network Substrate) — via tnsnames.ora
// Definisikan entri di $ORACLE_HOME/network/admin/tnsnames.ora:
// MYDB =
// (DESCRIPTION =
// (ADDRESS = (PROTOCOL = TCP)(HOST = db-server)(PORT = 1521))
// (CONNECT_DATA = (SERVICE_NAME = mydb.example.com))
// )
$koneksi = oci_connect('hr', 'welcome1', 'MYDB');
// 4. Connection string penuh (tanpa tnsnames.ora)
$tns = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))";
$koneksi = oci_connect('hr', 'welcome1', $tns);
// 5. Oracle Cloud (ATP/ADW) — butuh Wallet
putenv("TNS_ADMIN=/path/to/wallet"); // lokasi cwallet.sso dan tnsnames.ora
$koneksi = oci_connect('admin', 'MyCloudPassword123!', 'mydb_high');
if ($koneksi === false) {
$error = oci_error();
throw new \RuntimeException(
"Koneksi Oracle gagal: [{$error['code']}] {$error['message']}"
);
}
// Tutup koneksi
oci_close($koneksi);
// oci_pconnect — persistent connection (reuse dari connection pool proses)
$koneksiPersisten = oci_pconnect('hr', 'welcome1', '//localhost/ORCL');
// oci_new_connect — selalu buat koneksi baru (bukan dari pool)
$koneksiBaru = oci_new_connect('hr', 'welcome1', '//localhost/ORCL');
Koneksi via PDO_OCI #
<?php
// PDO dengan driver Oracle
$dsn = 'oci:dbname=//localhost:1521/ORCL;charset=AL32UTF8';
$pdo = new PDO($dsn, 'hr', 'welcome1', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
Prepared Statement dan Bind Variable #
Oracle menggunakan istilah bind variable untuk parameter dalam prepared statement. Ini bukan hanya keamanan (mencegah SQL injection) — di Oracle, bind variable juga meningkatkan performa secara signifikan karena query dengan bind variable bisa di-parse sekali dan dieksekusi berkali-kali dari shared pool.
<?php
// Query dengan bind variable
$sql = "SELECT id, nama, email FROM employees WHERE department_id = :dept AND salary > :min_salary";
$stmt = oci_parse($koneksi, $sql);
// Bind variable ke statement
oci_bind_by_name($stmt, ':dept', $deptId, -1, SQLT_INT);
oci_bind_by_name($stmt, ':min_salary', $minSalary, -1, SQLT_FLT);
$deptId = 10;
$minSalary = 5000.0;
oci_execute($stmt);
// Fetch hasil
while ($row = oci_fetch_assoc($stmt)) {
echo "{$row['ID']}: {$row['NAMA']} — {$row['EMAIL']}\n";
// Perhatian: nama kolom Oracle secara default UPPERCASE!
}
oci_free_statement($stmt);
// Fetch semua sekaligus
oci_execute($stmt);
oci_fetch_all($stmt, $hasil, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC);
print_r($hasil);
// INSERT dengan bind variable
$sql = "INSERT INTO produk (id, nama, harga, created_at)
VALUES (produk_seq.NEXTVAL, :nama, :harga, SYSDATE)";
$stmt = oci_parse($koneksi, $sql);
oci_bind_by_name($stmt, ':nama', $nama, 100, SQLT_CHR);
oci_bind_by_name($stmt, ':harga', $harga, -1, SQLT_FLT);
$nama = 'Laptop Pro';
$harga = 15000000.0;
oci_execute($stmt);
echo oci_num_rows($stmt) . " baris diinsert\n";
oci_free_statement($stmt);
Sequence — Auto-increment di Oracle #
Oracle tidak punya AUTO_INCREMENT — gunakan Sequence untuk menghasilkan nilai unik yang berurutan:
-- Buat sequence
CREATE SEQUENCE produk_seq
START WITH 1
INCREMENT BY 1
NOCACHE -- atau CACHE 20 untuk performa
NOCYCLE;
-- Gunakan di INSERT
INSERT INTO produk (id, nama) VALUES (produk_seq.NEXTVAL, 'Laptop');
-- Dapatkan nilai yang baru dihasilkan
SELECT produk_seq.CURRVAL FROM DUAL;
-- Oracle 12c+: IDENTITY column (seperti AUTO_INCREMENT)
CREATE TABLE produk (
id NUMBER GENERATED ALWAYS AS IDENTITY,
nama VARCHAR2(100)
);
<?php
// Insert dengan sequence dan ambil ID yang baru
$sql = "INSERT INTO produk (id, nama, harga)
VALUES (produk_seq.NEXTVAL, :nama, :harga)
RETURNING id INTO :new_id";
$stmt = oci_parse($koneksi, $sql);
oci_bind_by_name($stmt, ':nama', $nama, 100);
oci_bind_by_name($stmt, ':harga', $harga, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':new_id', $newId, -1, SQLT_INT); // output
$nama = 'Monitor 4K';
$harga = 5000000.0;
oci_execute($stmt);
echo "ID baru: $newId\n"; // nilai dari sequence
oci_free_statement($stmt);
Cursor dan REF CURSOR #
REF CURSOR adalah fitur Oracle yang sangat kuat — memungkinkan stored procedure mengembalikan result set yang bisa diiterasi dari PHP:
<?php
// Definisi stored procedure di Oracle (PL/SQL):
/*
CREATE OR REPLACE PROCEDURE get_employees_by_dept(
p_dept_id IN NUMBER,
p_cursor OUT SYS_REFCURSOR
) AS
BEGIN
OPEN p_cursor FOR
SELECT id, nama, email, salary
FROM employees
WHERE department_id = p_dept_id
ORDER BY nama;
END;
*/
// Panggil dari PHP
$sql = "BEGIN get_employees_by_dept(:dept_id, :cursor); END;";
$stmt = oci_parse($koneksi, $sql);
$cursor = oci_new_cursor($koneksi); // buat cursor kosong
oci_bind_by_name($stmt, ':dept_id', $deptId, -1, SQLT_INT);
oci_bind_by_name($stmt, ':cursor', $cursor, -1, OCI_B_CURSOR); // bind cursor
$deptId = 10;
oci_execute($stmt);
oci_execute($cursor); // eksekusi cursor yang sudah diisi stored procedure
// Iterasi hasil cursor
while ($row = oci_fetch_assoc($cursor)) {
echo "{$row['NAMA']}: {$row['EMAIL']}\n";
}
oci_free_statement($cursor);
oci_free_statement($stmt);
PL/SQL Anonymous Block #
PHP bisa mengeksekusi blok PL/SQL langsung — berguna untuk operasi batch atau logika yang lebih cocok di database:
<?php
// Eksekusi PL/SQL block
$plsql = "
BEGIN
UPDATE produk
SET harga = harga * (1 + :kenaikan_persen / 100)
WHERE kategori = :kategori;
INSERT INTO audit_log (aksi, detail, created_at)
VALUES ('UPDATE_HARGA', 'Kategori: ' || :kategori || ', Kenaikan: ' || :kenaikan_persen || '%', SYSDATE);
COMMIT;
END;
";
$stmt = oci_parse($koneksi, $plsql);
oci_bind_by_name($stmt, ':kenaikan_persen', $kenaikan, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':kategori', $kategori, 50, SQLT_CHR);
$kenaikan = 10.0; // 10% kenaikan
$kategori = 'ELEKTRONIK';
oci_execute($stmt, OCI_NO_AUTO_COMMIT); // tanpa auto-commit
// OCI8 auto-commit secara default — OCI_NO_AUTO_COMMIT untuk transaksi manual
oci_commit($koneksi); // commit manual
oci_free_statement($stmt);
LOB — Large Object (CLOB dan BLOB) #
Oracle menggunakan tipe LOB (Large Object) untuk data besar — CLOB untuk teks panjang, BLOB untuk binary:
<?php
// Menyimpan teks panjang (CLOB)
$sql = "INSERT INTO dokumen (id, judul, konten) VALUES (dok_seq.NEXTVAL, :judul, EMPTY_CLOB())
RETURNING konten INTO :clob_var";
$stmt = oci_parse($koneksi, $sql);
$clob = oci_new_descriptor($koneksi, OCI_D_LOB); // buat LOB descriptor
oci_bind_by_name($stmt, ':judul', $judul, 200, SQLT_CHR);
oci_bind_by_name($stmt, ':clob_var', $clob, -1, OCI_B_CLOB);
$judul = 'Laporan Tahunan 2024';
$konten = str_repeat("Isi laporan yang sangat panjang. ", 10000); // teks besar
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
// Tulis konten ke LOB
$clob->saveFile = false;
$clob->write($konten);
oci_commit($koneksi);
oci_free_statement($stmt);
$clob->free();
// Membaca CLOB
$stmt = oci_parse($koneksi, "SELECT konten FROM dokumen WHERE id = :id");
oci_bind_by_name($stmt, ':id', $id, -1, SQLT_INT);
$id = 1;
oci_execute($stmt);
$row = oci_fetch_array($stmt, OCI_ASSOC + OCI_RETURN_LOBS);
$isiDokumen = $row['KONTEN']; // OCI_RETURN_LOBS otomatis load LOB ke string
oci_free_statement($stmt);
// Menyimpan file (BLOB)
$sql = "INSERT INTO files (id, nama, data) VALUES (file_seq.NEXTVAL, :nama, EMPTY_BLOB())
RETURNING data INTO :blob_var";
$stmt = oci_parse($koneksi, $sql);
$blob = oci_new_descriptor($koneksi, OCI_D_LOB);
oci_bind_by_name($stmt, ':nama', $namaFile, 200, SQLT_CHR);
oci_bind_by_name($stmt, ':blob_var', $blob, -1, OCI_B_BLOB);
$namaFile = 'laporan.pdf';
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
$blob->saveFile('/path/to/laporan.pdf'); // baca dari file dan simpan ke BLOB
oci_commit($koneksi);
$blob->free();
oci_free_statement($stmt);
Transaksi di Oracle #
<?php
// OCI8 auto-commit secara default — harus eksplisit untuk transaksi
function transferDana(mixed $koneksi, int $dariId, int $keId, float $jumlah): void
{
// 1. Kurangi saldo pengirim (dengan SELECT FOR UPDATE — lock baris)
$stmt = oci_parse($koneksi, "
SELECT saldo FROM rekening WHERE id = :id FOR UPDATE
");
oci_bind_by_name($stmt, ':id', $dariId, -1, SQLT_INT);
oci_execute($stmt, OCI_NO_AUTO_COMMIT); // jangan commit dulu
$row = oci_fetch_assoc($stmt);
if (!$row || $row['SALDO'] < $jumlah) {
oci_rollback($koneksi);
throw new \DomainException("Saldo tidak mencukupi");
}
oci_free_statement($stmt);
// 2. Kurangi
$stmt = oci_parse($koneksi, "UPDATE rekening SET saldo = saldo - :jumlah WHERE id = :id");
oci_bind_by_name($stmt, ':jumlah', $jumlah, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':id', $dariId, -1, SQLT_INT);
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
oci_free_statement($stmt);
// 3. Tambah ke penerima
$stmt = oci_parse($koneksi, "UPDATE rekening SET saldo = saldo + :jumlah WHERE id = :id");
oci_bind_by_name($stmt, ':jumlah', $jumlah, -1, SQLT_FLT);
oci_bind_by_name($stmt, ':id', $keId, -1, SQLT_INT);
oci_execute($stmt, OCI_NO_AUTO_COMMIT);
oci_free_statement($stmt);
// 4. Commit semua perubahan
oci_commit($koneksi);
}
// Rollback jika ada error
try {
transferDana($koneksi, 1, 2, 500000.0);
} catch (\Throwable $e) {
oci_rollback($koneksi);
throw $e;
}
Perbedaan Penting Oracle vs MySQL #
-- 1. Dual table — untuk query tanpa tabel
SELECT SYSDATE FROM DUAL; -- Oracle
SELECT NOW(); -- MySQL
-- 2. Concatenation
SELECT 'Halo' || ' ' || nama FROM employees; -- Oracle
SELECT CONCAT('Halo', ' ', nama) FROM users; -- MySQL
-- 3. String: single quote only
SELECT 'teks' FROM DUAL; -- Oracle (double quote untuk identifier)
-- 4. Pagination
-- Oracle 12c+: OFFSET ... FETCH
SELECT * FROM produk ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- Oracle 11g: ROWNUM (trickier)
SELECT * FROM (
SELECT p.*, ROWNUM rn FROM (
SELECT * FROM produk ORDER BY id
) p WHERE ROWNUM <= 30
) WHERE rn > 20;
-- 5. Nama kolom uppercase default
-- Oracle mengembalikan nama kolom sebagai UPPERCASE
-- Pastikan akses dengan uppercase: $row['NAMA'], bukan $row['nama']
-- 6. NULL concatenation
SELECT 'a' || NULL FROM DUAL; -- 'a' (NULL diabaikan di Oracle)
SELECT CONCAT('a', NULL); -- NULL (di MySQL)
-- 7. Empty string = NULL di Oracle!
INSERT INTO t (col) VALUES (''); -- NULL di Oracle, bukan string kosong
Ringkasan #
- OCI8 adalah pilihan utama untuk Oracle — mendukung fitur khas Oracle seperti REF CURSOR, LOB streaming, array binding, dan DRCP connection pool yang tidak tersedia penuh di PDO_OCI.
- Oracle Instant Client wajib diinstal sebelum menginstal ekstensi OCI8 — ini library C dari Oracle yang menjadi jembatan antara PHP dan database.
- Bind variable di Oracle bukan hanya untuk keamanan — ia juga meningkatkan performa karena Oracle meng-cache query plan di shared pool untuk bind variable yang sama.
- Nama kolom Oracle adalah UPPERCASE secara default — akses hasil fetch dengan
$row['NAMA'], bukan$row['nama']. GunakanAS "nama"(dengan kutip) jika ingin lowercase.- Sequence menggantikan AUTO_INCREMENT — buat dengan
CREATE SEQUENCE nama_seqdan gunakanNEXTVALsaat INSERT. Oracle 12c+ mendukungIDENTITYcolumn.- REF CURSOR adalah cara Oracle mengembalikan result set dari stored procedure — buat dengan
oci_new_cursor(), bind sebagaiOCI_B_CURSOR, eksekusi secara terpisah, lalu fetch.- OCI8 auto-commit secara default — gunakan
OCI_NO_AUTO_COMMITsaatoci_execute()untuk transaksi manual, laluoci_commit()atauoci_rollback().- Empty string = NULL di Oracle — berbeda dari MySQL/PostgreSQL. Simpan string kosong yang memang kosong dengan spasi atau ubah desain skema.