Oracle #
Oracle Database adalah salah satu database enterprise paling matang dan banyak digunakan di perusahaan besar, lembaga keuangan, dan instansi pemerintahan. Mengintegrasikannya dengan TypeScript menggunakan library oracledb dari Oracle sendiri memberi akses ke fitur-fitur canggih seperti REF CURSOR, PL/SQL anonymous blocks, LOB (Large Objects), dan RETURNING INTO — semuanya dengan dukungan TypeScript yang cukup baik. Ada beberapa karakteristik Oracle yang perlu kamu pahami sejak awal: bind parameters menggunakan prefix : (bukan ? atau @), nama tabel dan kolom secara default case-insensitive dan disimpan dalam huruf kapital, dan library ini memerlukan Oracle Client atau Instant Client yang terinstal di sistem. Memahami keunikan-keunikan ini akan menghindarkan kamu dari kebingungan yang sering dialami developer saat pertama kali bekerja dengan Oracle.
Instalasi dan Setup #
oracledb memerlukan Oracle Instant Client yang terinstal di sistem operasi, selain paket npm-nya. Ini berbeda dari mysql2 dan mssql yang merupakan pure JavaScript.
# Instalasi paket npm
npm install oracledb
# Type definitions — perlu instalasi terpisah
npm install --save-dev @types/oracledb
Instalasi Oracle Instant Client #
# macOS (via Homebrew)
brew install instantclient-basic
# Ubuntu/Debian
# Download dari: https://www.oracle.com/database/technologies/instant-client/downloads.html
# Ekstrak ke /opt/oracle/instantclient_21_x
sudo apt-get install libaio1
export LD_LIBRARY_PATH=/opt/oracle/instantclient_21_x:$LD_LIBRARY_PATH
# Windows — tambahkan folder Instant Client ke PATH
// src/db/connection.ts — inisialisasi oracledb
import oracledb, {
Connection,
Pool,
BindParameters,
ExecuteOptions,
} from 'oracledb';
// Konfigurasi global — panggil sekali saat startup
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT; // hasil sebagai objek, bukan array
oracledb.autoCommit = false; // selalu eksplisit commit
oracledb.fetchArraySize = 100; // jumlah baris per fetch dari server
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
Struktur project yang direkomendasikan:
src/
├── db/
│ ├── connection.ts -- pool singleton dan konfigurasi global
│ └── types.ts -- tipe Oracle custom dan helper
├── models/
│ ├── user.model.ts
│ └── produk.model.ts
├── repositories/
│ ├── base.repository.ts
│ └── user.repository.ts
└── index.ts
Konfigurasi Connection Pool #
Oracle menggunakan createPool() untuk membuat pool koneksi. Pool Oracle punya fitur unik bernama sessionCallback yang memungkinkan inisialisasi sesi saat koneksi pertama kali diambil dari pool.
import oracledb, { Pool, Connection, PoolAttributes } from 'oracledb';
const poolConfig: PoolAttributes = {
user: process.env.DB_USER ?? 'system',
password: process.env.DB_PASSWORD ?? '',
connectString: process.env.DB_CONNECT_STRING ?? 'localhost:1521/XEPDB1',
// Format connectString:
// - Easy Connect: "host:port/service_name"
// - TNS alias: "MYDB" (butuh tnsnames.ora)
// - Full: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=host)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=svc)))"
poolMin: 2, // koneksi minimum yang selalu hidup
poolMax: 10, // koneksi maksimum
poolIncrement: 1, // tambah koneksi sebanyak ini saat butuh
poolTimeout: 60, // detik sebelum koneksi idle dihapus dari pool
poolPingInterval: 60, // cek koneksi idle setiap N detik
sessionCallback: initSession, // panggil saat koneksi baru dibuat
};
// Inisialisasi sesi — dijalankan sekali per koneksi baru di pool
async function initSession(
connection: Connection,
requestedTag: string,
callback: (error?: Error) => void
): Promise<void> {
try {
// Set timezone, format tanggal, dan parameter sesi lainnya
await connection.execute(
`ALTER SESSION SET
NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'
NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'
TIME_ZONE = 'Asia/Jakarta'`
);
callback();
} catch (err) {
callback(err instanceof Error ? err : new Error(String(err)));
}
}
let poolInstance: Pool | null = null;
export async function getPool(): Promise<Pool> {
if (!poolInstance) {
poolInstance = await oracledb.createPool(poolConfig);
}
return poolInstance;
}
export async function getConnection(): Promise<Connection> {
const pool = await getPool();
return pool.getConnection();
}
export async function closePool(): Promise<void> {
if (poolInstance) {
await poolInstance.close(10); // tunggu max 10 detik untuk koneksi aktif selesai
poolInstance = null;
}
}
process.on('SIGTERM', closePool);
process.on('SIGINT', closePool);
flowchart TD
A[oracledb.createPool] --> B[Pool siap\nmin: 2 koneksi]
B --> C[Request masuk]
C --> D[pool.getConnection]
D --> E{Ada koneksi\nidle di pool?}
E -- Ya --> F[Ambil koneksi\nidle]
E -- Tidak --> G{Jumlah < poolMax?}
G -- Ya --> H[Buat koneksi baru\njalankan sessionCallback]
G -- Tidak --> I[Tunggu koneksi\ntersedia]
F --> J[Eksekusi query]
H --> J
I --> F
J --> K[connection.close\nkembalikan ke pool]
K --> BBerbeda darimysql2danmssql,oracledbmemerlukanconnection.close()— bukanconnection.release()— untuk mengembalikan koneksi ke pool. Memanggilclose()pada koneksi yang berasal dari pool secara otomatis mengembalikannya ke pool, bukan benar-benar menutupnya. Lupa memanggilclose()akan menghabiskan slot pool secara permanen.
Mendefinisikan Tipe untuk Row Database #
Saat menggunakan oracledb.OUT_FORMAT_OBJECT, hasil query dikembalikan sebagai array objek. Nama kolom yang dikembalikan adalah huruf kapital semua sesuai konvensi Oracle, kecuali jika kamu menggunakan alias kolom dengan huruf kecil dalam query.
// Tipe untuk tabel USERS (nama kolom Oracle selalu kapital)
interface UserRow {
ID: number;
NAMA: string;
EMAIL: string;
PASSWORD_HASH: string;
ROLE: string;
AKTIF: number; // Oracle tidak punya BOOLEAN native — gunakan NUMBER(1)
DIBUAT_PADA: Date;
DIPERBARUI_PADA: Date | null;
}
// Tipe yang lebih friendly untuk digunakan di aplikasi
interface User {
id: number;
nama: string;
email: string;
passwordHash: string;
role: 'admin' | 'user' | 'moderator';
aktif: boolean;
dibuatPada: Date;
diperbarui_pada: Date | null;
}
// Fungsi mapper dari Oracle row ke domain type
function mapRowToUser(row: UserRow): User {
return {
id: row.ID,
nama: row.NAMA,
email: row.EMAIL,
passwordHash: row.PASSWORD_HASH,
role: row.ROLE as User['role'],
aktif: row.AKTIF === 1,
dibuatPada: row.DIBUAT_PADA,
diperbarui_pada: row.DIPERBARUI_PADA,
};
}
Cara alternatif yang lebih ringkas: gunakan alias kolom huruf kecil langsung di query SQL.
// Dengan alias lowercase di SQL — hasilnya langsung camelCase-friendly
interface UserFromQuery {
id: number;
nama: string;
email: string;
aktif: number;
dibuat_pada: Date;
}
// Query dengan alias kolom explicit
const SQL_SELECT_USER = `
SELECT
u.id AS "id",
u.nama AS "nama",
u.email AS "email",
u.aktif AS "aktif",
u.dibuat_pada AS "dibuat_pada"
FROM users u
WHERE u.id = :id
`;
// Kolom dalam double-quote di Oracle menjadi case-sensitive
Pemetaan tipe data Oracle ke TypeScript:
| Oracle Type | TypeScript Type | Catatan |
|---|---|---|
NUMBER, INTEGER | number | Hati-hati untuk BIGINT |
NUMBER(1) | number (0/1) | Oracle tidak punya BOOLEAN native |
VARCHAR2(n) | string | Maksimum 4000 karakter |
NVARCHAR2(n) | string | Unicode, maksimum 2000 karakter |
CLOB | string | Text panjang, perlu fetch khusus |
DATE | Date | Termasuk komponen waktu |
TIMESTAMP | Date | Presisi hingga nanosecond |
BLOB | Buffer | Data biner |
Query Dasar — SELECT #
Oracle menggunakan :namaParam sebagai placeholder bind parameter. Berbeda dari MySQL dan MSSQL, kamu bisa menggunakan bind by position (:1, :2) atau bind by name (:namaParam).
import oracledb from 'oracledb';
import { getConnection } from './db/connection';
// ── SELECT sederhana
async function semuaUser(): Promise<User[]> {
const conn = await getConnection();
try {
const result = await conn.execute<UserRow>(
`SELECT id, nama, email, role, aktif, dibuat_pada
FROM users
WHERE aktif = 1
ORDER BY dibuat_pada DESC`
);
return (result.rows ?? []).map(mapRowToUser);
} finally {
await conn.close(); // WAJIB: selalu close di finally
}
}
// ── SELECT dengan bind parameter — gunakan :namaParam
async function cariUserById(id: number): Promise<User | null> {
const conn = await getConnection();
try {
const result = await conn.execute<UserRow>(
'SELECT * FROM users WHERE id = :id AND aktif = 1',
{ id } // bind by name: { id: value }
);
const row = result.rows?.[0];
return row ? mapRowToUser(row) : null;
} finally {
await conn.close();
}
}
// ── SELECT dengan multiple bind parameter
async function produkByKategoriDanHarga(
kategoriId: number,
hargaMin: number,
hargaMax: number
): Promise<Produk[]> {
const conn = await getConnection();
try {
const result = await conn.execute<ProdukRow>(
`SELECT *
FROM produk
WHERE kategori_id = :kategoriId
AND harga BETWEEN :hargaMin AND :hargaMax
AND stok > 0
ORDER BY harga ASC`,
{ kategoriId, hargaMin, hargaMax }
);
return (result.rows ?? []).map(mapRowToProduk);
} finally {
await conn.close();
}
}
// ── SELECT dengan LIKE — Oracle menggunakan || untuk concatenation
async function cariProdukByNama(keyword: string): Promise<Produk[]> {
const conn = await getConnection();
try {
// Escape karakter khusus LIKE Oracle: %, _, \
const safeKeyword = keyword.replace(/[%_\\]/g, '\\$&');
const result = await conn.execute<ProdukRow>(
`SELECT * FROM produk
WHERE LOWER(nama) LIKE LOWER(:keyword) ESCAPE '\'
FETCH FIRST 20 ROWS ONLY`,
{ keyword: `%${safeKeyword}%` }
);
return (result.rows ?? []).map(mapRowToProduk);
} finally {
await conn.close();
}
}
// ── SELECT dengan IN — Oracle tidak bisa bind array langsung
async function produkByIds(ids: number[]): Promise<Produk[]> {
if (ids.length === 0) return [];
const conn = await getConnection();
try {
// Buat bind variables dinamis: :id0, :id1, :id2, ...
const binds: Record<string, number> = {};
const placeholders = ids.map((id, i) => {
binds[`id${i}`] = id;
return `:id${i}`;
});
const result = await conn.execute<ProdukRow>(
`SELECT * FROM produk WHERE id IN (${placeholders.join(', ')})`,
binds
);
return (result.rows ?? []).map(mapRowToProduk);
} finally {
await conn.close();
}
}
// ANTI-PATTERN: string interpolasi — rentan SQL injection
async function cariUserTidakAman(email: string): Promise<UserRow[]> {
const conn = await getConnection();
try {
const result = await conn.execute<UserRow>(
`SELECT * FROM users WHERE email = '${email}'` // JANGAN!
);
return result.rows ?? [];
} finally {
await conn.close();
}
}
// BENAR: selalu gunakan bind parameter
async function cariUserAman(email: string): Promise<User | null> {
const conn = await getConnection();
try {
const result = await conn.execute<UserRow>(
'SELECT * FROM users WHERE email = :email',
{ email } // nilai diproses terpisah oleh Oracle — aman
);
const row = result.rows?.[0];
return row ? mapRowToUser(row) : null;
} finally {
await conn.close();
}
}
Operasi INSERT #
Oracle menggunakan klausa RETURNING INTO untuk mendapatkan nilai yang baru diinsert — ekuivalen dengan OUTPUT INSERTED di SQL Server.
import oracledb from 'oracledb';
import { getConnection } from './db/connection';
interface InputUser {
nama: string;
email: string;
passwordHash: string;
role?: string;
}
// ── INSERT dengan RETURNING INTO untuk mendapat ID
async function buatUser(input: InputUser): Promise<number> {
const conn = await getConnection();
try {
const result = await conn.execute(
`INSERT INTO users (id, nama, email, password_hash, role, aktif, dibuat_pada)
VALUES (users_seq.NEXTVAL, :nama, :email, :passwordHash, :role, 1, SYSDATE)
RETURNING id INTO :newId`,
{
nama: input.nama,
email: input.email,
passwordHash: input.passwordHash,
role: input.role ?? 'user',
newId: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
}
);
await conn.commit();
// RETURNING INTO dikembalikan dalam outBinds
const outBinds = result.outBinds as { newId: number[] };
return outBinds.newId[0];
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.close();
}
}
// ── INSERT batch dengan executemany — jauh lebih efisien dari loop
async function buatBanyakProduk(
produkList: Array<{ nama: string; harga: number; stok: number; kategoriId: number }>
): Promise<number> {
if (produkList.length === 0) return 0;
const conn = await getConnection();
try {
const binds = produkList.map(p => ({
nama: p.nama,
harga: p.harga,
stok: p.stok,
kategoriId: p.kategoriId,
}));
const result = await conn.executeMany(
`INSERT INTO produk (id, nama, harga, stok, kategori_id, dibuat_pada)
VALUES (produk_seq.NEXTVAL, :nama, :harga, :stok, :kategoriId, SYSDATE)`,
binds,
{ autoCommit: true } // commit otomatis setelah semua baris diinsert
);
return result.rowsAffected ?? 0;
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.close();
}
}
sequenceDiagram
participant App
participant Oracle
participant Sequence
App->>Oracle: INSERT ... VALUES (seq.NEXTVAL, :params)\nRETURNING id INTO :newId
Oracle->>Sequence: seq.NEXTVAL
Sequence-->>Oracle: 42
Oracle->>Oracle: Simpan row dengan id=42
Oracle-->>App: outBinds.newId = [42]
App->>Oracle: conn.commit()
Oracle-->>App: OKOracle tidak punya auto-increment kolom seperti MySQL (kecuali kolomGENERATED ALWAYS AS IDENTITYdi Oracle 12c+). Cara tradisional menggunakan Sequence (CREATE SEQUENCE nama_seq) yang dipanggil dengannama_seq.NEXTVAL. Pastikan sequence sudah dibuat di database sebelum menjalankan INSERT, atau gunakanGENERATED ALWAYS AS IDENTITYjika Oracle versi 12c ke atas.
Operasi UPDATE dan DELETE #
import oracledb from 'oracledb';
import { getConnection } from './db/connection';
// ── UPDATE dengan RETURNING INTO
async function updateStokProduk(
id: number,
deltaStok: number
): Promise<{ stokBaru: number } | null> {
const conn = await getConnection();
try {
const result = await conn.execute(
`UPDATE produk
SET stok = stok + :delta
WHERE id = :id AND (stok + :delta2) >= 0
RETURNING stok INTO :stokBaru`,
{
id,
delta: deltaStok,
delta2: deltaStok, // Oracle perlu bind terpisah untuk nilai yang sama
stokBaru: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
}
);
await conn.commit();
const out = result.outBinds as { stokBaru: number[] };
if (!out.stokBaru || out.stokBaru.length === 0) return null;
return { stokBaru: out.stokBaru[0] };
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.close();
}
}
// ── UPDATE field tertentu
async function updateUser(
id: number,
data: Partial<Pick<User, 'nama' | 'email'>>
): Promise<boolean> {
const entries = Object.entries(data).filter(([, v]) => v !== undefined);
if (entries.length === 0) return false;
// Whitelist — WAJIB untuk nama kolom yang diinterpolasi
const allowedColumns: Record<string, string> = {
nama: 'nama',
email: 'email',
};
const setClauses: string[] = [];
const binds: Record<string, unknown> = { id };
for (const [key, value] of entries) {
const col = allowedColumns[key];
if (!col) continue;
setClauses.push(`${col} = :${key}`);
binds[key] = value;
}
if (setClauses.length === 0) return false;
const conn = await getConnection();
try {
const result = await conn.execute(
`UPDATE users
SET ${setClauses.join(', ')}, diperbarui_pada = SYSDATE
WHERE id = :id`,
binds
);
await conn.commit();
return (result.rowsAffected ?? 0) > 0;
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.close();
}
}
// ── DELETE
async function hapusUser(id: number): Promise<boolean> {
const conn = await getConnection();
try {
const result = await conn.execute(
'DELETE FROM users WHERE id = :id',
{ id }
);
await conn.commit();
return (result.rowsAffected ?? 0) > 0;
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.close();
}
}
Transaction #
Di Oracle, setiap sesi database secara implisit memulai transaksi saat query pertama dieksekusi. Commit atau rollback mengakhiri transaksi dan memulai yang baru. Tidak ada perintah BEGIN TRANSACTION yang eksplisit seperti di MySQL atau SQL Server.
import oracledb from 'oracledb';
import { getConnection } from './db/connection';
interface ItemPesanan {
produkId: number;
jumlah: number;
hargaSatuan: number;
}
async function buatPesanan(
userId: number,
items: ItemPesanan[]
): Promise<number> {
const conn = await getConnection();
try {
// Oracle: transaksi dimulai otomatis saat query pertama
const totalHarga = items.reduce(
(sum, item) => sum + item.hargaSatuan * item.jumlah, 0
);
// 1. Insert pesanan
const r1 = await conn.execute(
`INSERT INTO pesanan (id, user_id, total_harga, status, dibuat_pada)
VALUES (pesanan_seq.NEXTVAL, :userId, :total, 'pending', SYSDATE)
RETURNING id INTO :newId`,
{
userId,
total: totalHarga,
newId: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
}
);
const out1 = r1.outBinds as { newId: number[] };
const pesananId = out1.newId[0];
// 2. Proses setiap item
for (const item of items) {
await conn.execute(
`INSERT INTO pesanan_item
(id, pesanan_id, produk_id, jumlah, harga_satuan)
VALUES (pesanan_item_seq.NEXTVAL, :pesananId, :produkId, :jumlah, :harga)`,
{
pesananId,
produkId: item.produkId,
jumlah: item.jumlah,
harga: item.hargaSatuan,
}
);
// Kurangi stok — gagal jika stok tidak cukup
const rStok = await conn.execute(
`UPDATE produk
SET stok = stok - :jumlah
WHERE id = :produkId AND stok >= :jumlah2`,
{ jumlah: item.jumlah, produkId: item.produkId, jumlah2: item.jumlah }
);
if ((rStok.rowsAffected ?? 0) === 0) {
await conn.rollback();
throw new Error(`Stok tidak cukup untuk produk ID ${item.produkId}`);
}
}
// Commit hanya jika semua langkah berhasil
await conn.commit();
return pesananId;
} catch (error) {
try { await conn.rollback(); } catch { /* abaikan error rollback */ }
throw error;
} finally {
await conn.close();
}
}
flowchart TD
A[conn = pool.getConnection] --> B[Transaksi dimulai\notomatis saat query pertama]
B --> C[INSERT pesanan]
C --> D[Loop items]
D --> E[INSERT pesanan_item]
E --> F[UPDATE stok]
F --> G{rowsAffected > 0?}
G -- Tidak --> H[conn.rollback\nThrow Error]
G -- Ya --> I{Masih ada\nitem?}
I -- Ya --> D
I -- Tidak --> J[conn.commit]
H --> K[conn.close]
J --> KStored Procedure dan PL/SQL #
Oracle sangat erat dengan PL/SQL. oracledb mendukung pemanggilan stored procedure dan PL/SQL anonymous blocks secara native.
import oracledb from 'oracledb';
import { getConnection } from './db/connection';
// ── Memanggil stored procedure
async function spCariUser(email: string): Promise<User | null> {
const conn = await getConnection();
try {
const result = await conn.execute(
'BEGIN sp_cari_user(:email, :cursor); END;',
{
email,
cursor: { dir: oracledb.BIND_OUT, type: oracledb.CURSOR },
}
);
const out = result.outBinds as { cursor: oracledb.ResultSet<UserRow> };
const cursor = out.cursor;
const rows = await cursor.getRows<UserRow>(1);
await cursor.close();
return rows[0] ? mapRowToUser(rows[0]) : null;
} finally {
await conn.close();
}
}
// ── Stored procedure dengan OUTPUT parameter
async function spDaftarUser(
input: InputUser
): Promise<{ userId: number; pesanError: string | null }> {
const conn = await getConnection();
try {
const result = await conn.execute(
`BEGIN
sp_daftar_user(
p_nama => :nama,
p_email => :email,
p_password => :password,
p_user_id => :userId,
p_pesan_error => :pesanError
);
END;`,
{
nama: input.nama,
email: input.email,
password: input.passwordHash,
userId: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
pesanError: { dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 500 },
}
);
await conn.commit();
const out = result.outBinds as { userId: number; pesanError: string | null };
return { userId: out.userId, pesanError: out.pesanError };
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.close();
}
}
// ── PL/SQL anonymous block — untuk logika yang tidak perlu jadi SP permanen
async function prosesDataBatch(batchId: number): Promise<{ berhasil: number; gagal: number }> {
const conn = await getConnection();
try {
const result = await conn.execute(
`DECLARE
v_berhasil NUMBER := 0;
v_gagal NUMBER := 0;
BEGIN
FOR rec IN (SELECT id FROM antrian_proses WHERE batch_id = :batchId)
LOOP
BEGIN
UPDATE produk SET stok = stok + 1 WHERE id = rec.id;
v_berhasil := v_berhasil + 1;
EXCEPTION
WHEN OTHERS THEN
v_gagal := v_gagal + 1;
END;
END LOOP;
:berhasil := v_berhasil;
:gagal := v_gagal;
END;`,
{
batchId,
berhasil: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
gagal: { dir: oracledb.BIND_OUT, type: oracledb.NUMBER },
}
);
await conn.commit();
const out = result.outBinds as { berhasil: number; gagal: number };
return { berhasil: out.berhasil, gagal: out.gagal };
} finally {
await conn.close();
}
}
Error Handling #
Oracle menggunakan kode error ORA-XXXXX. Memahami kode-kode umum ini dan memetakannya ke domain error yang bermakna adalah kunci untuk pesan error yang informatif.
import oracledb from 'oracledb';
// Kode error Oracle yang paling sering ditemui
const ORA_ERRORS = {
UNIQUE_VIOLATION: 1, // ORA-00001: unique constraint violated
INTEGRITY_VIOLATION: 2291, // ORA-02291: integrity constraint (FK) violated
CHILD_EXISTS: 2292, // ORA-02292: child record found (delete FK)
NOT_NULL_VIOLATION: 1400, // ORA-01400: cannot insert NULL
VALUE_TOO_LARGE: 12899, // ORA-12899: value too large for column
DEADLOCK: 60, // ORA-00060: deadlock detected
SNAPSHOT_TOO_OLD: 1555, // ORA-01555: snapshot too old
TABLE_NOT_EXIST: 942, // ORA-00942: table or view does not exist
SEQUENCE_NOT_EXIST: 2289, // ORA-02289: sequence does not exist
INVALID_NUMBER: 1722, // ORA-01722: invalid number
} as const;
class DatabaseError extends Error {
constructor(message: string, public readonly code?: number) {
super(message);
this.name = 'DatabaseError';
}
}
class UniqueViolationError extends DatabaseError {
constructor(constraint?: string) {
super(`Duplikat data${constraint ? ` pada constraint '${constraint}'` : ''}`);
this.name = 'UniqueViolationError';
}
}
class ForeignKeyError extends DatabaseError {
constructor(message = 'Referensi data tidak valid') {
super(message);
this.name = 'ForeignKeyError';
}
}
class DeadlockError extends DatabaseError {
constructor() {
super('Deadlock terdeteksi — coba ulangi operasi');
this.name = 'DeadlockError';
}
}
function tanganiOracleError(error: unknown): never {
// oracledb error memiliki properti errorNum
if (error && typeof error === 'object' && 'errorNum' in error) {
const oraError = error as { errorNum: number; message: string };
switch (oraError.errorNum) {
case ORA_ERRORS.UNIQUE_VIOLATION: {
// Ekstrak nama constraint dari pesan: ORA-00001: unique constraint (SCHEMA.CONST) violated
const match = oraError.message.match(/\((.+?)\)/);
throw new UniqueViolationError(match?.[1]);
}
case ORA_ERRORS.INTEGRITY_VIOLATION:
throw new ForeignKeyError('Record referensi tidak ditemukan');
case ORA_ERRORS.CHILD_EXISTS:
throw new ForeignKeyError('Tidak bisa menghapus — masih ada data yang mereferensikan');
case ORA_ERRORS.NOT_NULL_VIOLATION:
throw new DatabaseError('Field wajib tidak boleh kosong', oraError.errorNum);
case ORA_ERRORS.VALUE_TOO_LARGE:
throw new DatabaseError('Data melebihi panjang maksimum kolom', oraError.errorNum);
case ORA_ERRORS.DEADLOCK:
throw new DeadlockError();
}
}
throw new DatabaseError(`Error Oracle: ${String(error)}`);
}
// Retry otomatis untuk deadlock
async function withDeadlockRetry<T>(
fn: () => Promise<T>,
maxRetry = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxRetry; attempt++) {
try {
return await fn();
} catch (error) {
if (error instanceof DeadlockError && attempt < maxRetry) {
await new Promise(res => setTimeout(res, 200 * attempt));
continue;
}
throw error;
}
}
throw new Error('Unreachable');
}
Paginasi di Oracle #
Oracle menggunakan OFFSET ... FETCH NEXT (tersedia sejak Oracle 12c). Untuk Oracle 11g ke bawah, gunakan ROWNUM atau ROW_NUMBER().
import oracledb from 'oracledb';
import { getConnection } from './db/connection';
type SortField = 'nama' | 'email' | 'dibuat_pada';
const ALLOWED_SORT = new Set<SortField>(['nama', 'email', 'dibuat_pada']);
interface PaginasiResult<T> {
data: T[];
total: number;
halaman: number;
perHalaman: number;
totalHalaman: number;
}
// ── Oracle 12c+ — OFFSET...FETCH
async function getUserPaginasi(
halaman: number,
perHalaman: number,
sortBy: SortField = 'dibuat_pada',
sortOrder: 'ASC' | 'DESC' = 'DESC'
): Promise<PaginasiResult<User>> {
const safeSort = ALLOWED_SORT.has(sortBy) ? sortBy : 'dibuat_pada';
const safeOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC';
const offset = (halaman - 1) * perHalaman;
const conn = await getConnection();
try {
const [dataResult, countResult] = await Promise.all([
conn.execute<UserRow>(
`SELECT id, nama, email, role, aktif, dibuat_pada
FROM users
WHERE aktif = 1
ORDER BY ${safeSort} ${safeOrder}
OFFSET :offset ROWS FETCH NEXT :perHalaman ROWS ONLY`,
{ offset, perHalaman }
),
conn.execute<{ TOTAL: number }>(
'SELECT COUNT(*) AS TOTAL FROM users WHERE aktif = 1'
),
]);
const total = countResult.rows?.[0]?.TOTAL ?? 0;
return {
data: (dataResult.rows ?? []).map(mapRowToUser),
total,
halaman,
perHalaman,
totalHalaman: Math.ceil(total / perHalaman),
};
} finally {
await conn.close();
}
}
// ── Oracle 11g dan lebih lama — ROW_NUMBER()
async function getUserPaginasiLegacy(
halaman: number,
perHalaman: number
): Promise<User[]> {
const conn = await getConnection();
try {
const rn_start = (halaman - 1) * perHalaman + 1;
const rn_end = halaman * perHalaman;
const result = await conn.execute<UserRow>(
`SELECT * FROM (
SELECT u.*, ROW_NUMBER() OVER (ORDER BY dibuat_pada DESC) AS rn
FROM users u
WHERE aktif = 1
)
WHERE rn BETWEEN :rnStart AND :rnEnd`,
{ rnStart: rn_start, rnEnd: rn_end }
);
return (result.rows ?? []).map(mapRowToUser);
} finally {
await conn.close();
}
}
Kapan Beralih ke Pendekatan Lain #
Tetap gunakan oracledb langsung jika:
✓ Aplikasi enterprise yang banyak menggunakan stored procedure PL/SQL
✓ Butuh fitur Oracle spesifik: REF CURSOR, RETURNING INTO, executeMany
✓ Performa kritis dengan query yang sudah dioptimasi DBA
✓ Tim sudah familiar dengan PL/SQL dan Oracle
✓ Menggunakan fitur khusus Oracle: Partitioning, Advanced Queuing, Flashback
Pertimbangkan ORM / Query Builder jika:
✗ Banyak model dengan relasi — TypeORM mendukung Oracle via oracledb driver
✗ Tim kurang familiar PL/SQL — ORM membantu tapi bisa menyembunyikan performa
✗ Butuh migrasi otomatis — TypeORM migrations untuk Oracle
✗ Multi-database — TypeORM atau Knex.js (Knex support Oracle terbatas)
✗ Rapid prototyping — TypeORM lebih cepat untuk development awal
| Aspek | oracledb | TypeORM + Oracle | Knex.js + Oracle |
|---|---|---|---|
| Kontrol SQL | Penuh | Sebagian | Baik |
| PL/SQL / Stored Proc | ✓ Native | Terbatas | Terbatas |
| REF CURSOR | ✓ | Tidak | Tidak |
| RETURNING INTO | ✓ | Tidak langsung | Tidak |
| Migrasi otomatis | Manual | ✓ | Via Knex |
| Learning curve | Sedang | Sedang | Rendah |
Ringkasan #
connection.close()mengembalikan ke pool, tidak menutup permanen — selalu panggil di blokfinally; lupa memanggilclose()menghabiskan slot pool secara permanen.oracledb.outFormat = OUT_FORMAT_OBJECTharus di-set global di awal aplikasi — tanpa ini hasil query dikembalikan sebagai array posisional yang sulit dipakai dengan tipe TypeScript.- Nama kolom Oracle selalu kapital secara default (
ID,NAMA, bukanid,nama) — gunakan alias kolom dengan double-quote dalam SQL atau buat fungsi mapper dariUserRowkeUser.- Bind parameter menggunakan
:namaParam— berbeda dari MySQL (?) dan MSSQL (@nama); bind by name lebih aman dan mudah dibaca daripada bind by position (:1,:2).- Tidak ada auto-increment bawaan sebelum Oracle 12c — gunakan SEQUENCE dengan
seq.NEXTVALdi INSERT, atauGENERATED ALWAYS AS IDENTITYuntuk Oracle 12c ke atas.RETURNING INTOuntuk mendapat nilai setelah INSERT/UPDATE — deklarasikan bind output dengan{ dir: oracledb.BIND_OUT, type: oracledb.NUMBER }.executeManyuntuk bulk insert — jauh lebih efisien dari loop satu per satu; serahkan array bind ke driver dan biarkan Oracle mengoptimalkan pengirimannya.- Transaksi dimulai otomatis saat query pertama di Oracle — tidak ada
BEGIN TRANSACTION; selalu akhiri dengancommit()ataurollback()secara eksplisit karenaautoCommitsebaiknyafalse.sessionCallbackdi pool config untuk inisialisasi sesi — tempat yang tepat untukALTER SESSIONseperti NLS format tanggal dan timezone agar konsisten di seluruh koneksi.- Nama bind yang sama butuh alias berbeda — jika nilai yang sama dipakai dua kali dalam satu query, buat dua bind dengan nama berbeda (
:deltadan:delta2); Oracle tidak bisa menggunakan satu bind dua kali dalam konteks berbeda.