Oracle

Oracle #

Oracle Database adalah sistem manajemen basis data relasional enterprise paling kuat dan paling banyak digunakan di korporasi besar, perbankan, dan pemerintahan. Oracle punya dialek SQL dan fitur yang cukup berbeda dari MySQL dan MSSQL — memahami perbedaan ini adalah kunci agar tidak frustasi saat bermigrasi atau mengintegrasikan aplikasi Kotlin dengan Oracle. Beberapa perbedaan mencolok: tidak ada AUTO_INCREMENT (gunakan SEQUENCE), tidak ada BOOLEAN native (gunakan NUMBER(1)), string kosong dianggap NULL, dan DUAL sebagai tabel satu baris untuk ekspresi. Di Kotlin, kamu terhubung ke Oracle via JDBC driver resmi dari Oracle (ojdbc), dikombinasikan dengan HikariCP untuk connection pooling dan Exposed atau JDBC langsung untuk query.

Setup dan Dependensi #

Oracle JDBC driver (ojdbc) dulunya hanya bisa diunduh dari situs Oracle secara manual. Kini sudah tersedia di Maven Central:

// build.gradle.kts
dependencies {
    // Oracle JDBC Driver (ojdbc11 untuk JDK 11+, ojdbc8 untuk JDK 8)
    implementation("com.oracle.database.jdbc:ojdbc11:23.3.0.23.09")

    // Oracle Connection Pool (UCP — Universal Connection Pool, opsional)
    implementation("com.oracle.database.jdbc:ucp11:23.3.0.23.09")

    // HikariCP — lebih umum digunakan
    implementation("com.zaxxer:HikariCP:5.1.0")

    // Exposed ORM
    implementation("org.jetbrains.exposed:exposed-core:0.49.0")
    implementation("org.jetbrains.exposed:exposed-dao:0.49.0")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.49.0")

    // Flyway untuk Oracle
    implementation("org.flywaydb:flyway-core:10.10.0")
    implementation("org.flywaydb:flyway-database-oracle:10.10.0")
}

Format Connection String Oracle #

Oracle memiliki tiga format connection string yang berbeda:

// Format 1: SID (Service Identifier) — format lama
val urlSid = "jdbc:oracle:thin:@localhost:1521:ORCL"
// jdbc:oracle:thin:@HOST:PORT:SID

// Format 2: Service Name (lebih modern, direkomendasikan)
val urlService = "jdbc:oracle:thin:@//localhost:1521/orclpdb1"
// jdbc:oracle:thin:@//HOST:PORT/SERVICE_NAME

// Format 3: TNS dengan descriptor lengkap
val urlTns = """
    jdbc:oracle:thin:@(DESCRIPTION=
        (ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))
        (CONNECT_DATA=(SERVICE_NAME=orclpdb1)))
""".trimIndent().replace("\n", "").replace(" ", "")

// Oracle Cloud Database (ATP/ADW) — pakai wallet
val urlCloud = "jdbc:oracle:thin:@mydb_high?TNS_ADMIN=/path/to/wallet"

// Untuk development dengan Oracle XE (Express Edition)
val urlXe = "jdbc:oracle:thin:@//localhost:1521/xe"

Koneksi dengan HikariCP #

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource

object DatabaseOracle {
    private val dataSource: HikariDataSource by lazy {
        val config = HikariConfig().apply {
            jdbcUrl = "jdbc:oracle:thin:@//localhost:1521/orclpdb1"
            driverClassName = "oracle.jdbc.OracleDriver"
            username = System.getenv("DB_USER") ?: "myapp"
            password = System.getenv("DB_PASSWORD") ?: "password"

            // Pool configuration
            minimumIdle = 2
            maximumPoolSize = 10
            idleTimeout = 300_000
            connectionTimeout = 30_000
            maxLifetime = 1_800_000
            poolName = "Oracle-Pool"

            // Oracle-specific
            connectionTestQuery = "SELECT 1 FROM DUAL"  // Oracle butuh FROM DUAL

            // Properti tambahan untuk Oracle
            addDataSourceProperty("oracle.jdbc.implicitStatementCacheSize", "20")
            addDataSourceProperty("oracle.net.CONNECT_TIMEOUT", "10000")
            addDataSourceProperty("oracle.jdbc.ReadTimeout", "30000")
        }
        HikariDataSource(config)
    }

    fun <T> gunakan(blok: (java.sql.Connection) -> T): T {
        return dataSource.connection.use(blok)
    }

    fun tutup() {
        if (!dataSource.isClosed) dataSource.close()
    }
}

fun main() {
    DatabaseOracle.gunakan { koneksi ->
        koneksi.createStatement().use { stmt ->
            stmt.executeQuery("SELECT * FROM v\$version WHERE rownum = 1").use { rs ->
                if (rs.next()) println("Oracle: ${rs.getString(1)}")
            }
        }
    }
}

Perbedaan SQL Oracle vs MySQL/MSSQL #

Oracle punya sejumlah perbedaan sintaks yang cukup signifikan dan sering menjebak developer yang datang dari MySQL atau MSSQL:

Perbedaan Utama #

-- 1. AUTO INCREMENT → SEQUENCE
-- MySQL:   id INT AUTO_INCREMENT
-- MSSQL:   id INT IDENTITY(1,1)
-- Oracle:  Gunakan SEQUENCE + TRIGGER atau GENERATED ALWAYS AS IDENTITY (12c+)

-- Oracle 12c+ (direkomendasikan):
CREATE TABLE produk (
    id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama VARCHAR2(255) NOT NULL
);

-- Oracle versi lama (pre-12c):
CREATE SEQUENCE seq_produk START WITH 1 INCREMENT BY 1;
-- Lalu gunakan seq_produk.NEXTVAL di INSERT

-- 2. DUAL — tabel satu baris untuk ekspresi tanpa tabel
SELECT SYSDATE FROM DUAL;          -- waktu saat ini
SELECT UPPER('kotlin') FROM DUAL;  -- fungsi string
SELECT 1 + 1 FROM DUAL;            -- ekspresi aritmatika

-- 3. Paginasi
-- MySQL:  LIMIT 10 OFFSET 20
-- MSSQL:  OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
-- Oracle 12c+: OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY (sama dengan MSSQL)
-- Oracle lama: Gunakan subquery dengan ROWNUM

-- Oracle lama (pre-12c):
SELECT * FROM (
    SELECT t.*, ROWNUM AS rn FROM (
        SELECT * FROM produk WHERE aktif = 1 ORDER BY nama
    ) t WHERE ROWNUM <= 30  -- offset + limit
) WHERE rn > 20;            -- offset

-- 4. String kosong = NULL (gotcha terbesar Oracle!)
-- Di Oracle, '' dan NULL adalah hal yang sama untuk VARCHAR2
INSERT INTO pengguna (nama, bio) VALUES ('Budi', '');
-- bio tersimpan sebagai NULL, bukan string kosong!

-- 5. Tidak ada BOOLEAN native
-- MySQL:  BOOLEAN (alias TINYINT(1))
-- MSSQL:  BIT
-- Oracle: NUMBER(1) dengan konvensi 0=false, 1=true
-- Atau pakai CHAR(1) dengan 'Y'/'N'

-- 6. Tipe data teks
-- MySQL:  VARCHAR, TEXT
-- MSSQL:  NVARCHAR, NVARCHAR(MAX)
-- Oracle: VARCHAR2 (max 32767 char), CLOB (untuk teks besar)

-- 7. Fungsi yang berbeda
-- MySQL:  NOW(), IFNULL(), LIMIT
-- MSSQL:  GETDATE(), ISNULL(), TOP
-- Oracle: SYSDATE, NVL(), ROWNUM/FETCH FIRST

Schema Oracle Lengkap #

-- Buat sequence untuk ID (pre-12c)
CREATE SEQUENCE seq_produk
    START WITH 1
    INCREMENT BY 1
    NOCACHE          -- atau CACHE 20 untuk performa lebih baik
    NOCYCLE;

-- Buat tabel
CREATE TABLE produk (
    id              NUMBER          DEFAULT seq_produk.NEXTVAL PRIMARY KEY,
    -- atau Oracle 12c+: id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama            VARCHAR2(255)   NOT NULL,
    deskripsi       CLOB,                          -- untuk teks panjang
    harga           NUMBER(15,2)    NOT NULL,
    stok            NUMBER          DEFAULT 0 NOT NULL,
    kategori        VARCHAR2(100),
    aktif           NUMBER(1)       DEFAULT 1 NOT NULL,  -- 1=true, 0=false
    dibuat_pada     TIMESTAMP       DEFAULT SYSTIMESTAMP,
    diperbarui_pada TIMESTAMP       DEFAULT SYSTIMESTAMP,
    CONSTRAINT ck_aktif CHECK (aktif IN (0, 1)),
    CONSTRAINT ck_harga CHECK (harga >= 0),
    CONSTRAINT ck_stok CHECK (stok >= 0)
);

-- Index
CREATE INDEX idx_produk_kategori ON produk(kategori);
CREATE INDEX idx_produk_aktif ON produk(aktif);

-- Trigger untuk update timestamp otomatis (Oracle tidak punya ON UPDATE)
CREATE OR REPLACE TRIGGER trg_produk_update
BEFORE UPDATE ON produk
FOR EACH ROW
BEGIN
    :NEW.diperbarui_pada := SYSTIMESTAMP;
END;
/

CRUD dengan JDBC #

import java.math.BigDecimal
import java.sql.Clob
import java.sql.ResultSet
import java.sql.Types

data class Produk(
    val id: Long = 0,
    val nama: String,
    val deskripsi: String? = null,
    val harga: BigDecimal,
    val stok: Int = 0,
    val kategori: String? = null,
    val aktif: Boolean = true
)

class ProdukRepositoryOracle {

    private fun ResultSet.toProduk() = Produk(
        id        = getLong("id"),
        nama      = getString("nama"),
        // CLOB harus dibaca dengan cara khusus
        deskripsi = getClob("deskripsi")?.let { clob ->
            clob.getSubString(1, clob.length().toInt()).also { clob.free() }
        },
        harga     = getBigDecimal("harga"),
        stok      = getInt("stok"),
        kategori  = getString("kategori"),
        aktif     = getInt("aktif") == 1  // NUMBER(1) → Boolean
    )

    // INSERT — Oracle menggunakan RETURNING INTO untuk mendapat ID
    fun simpan(produk: Produk): Produk {
        val sql = """
            INSERT INTO produk (nama, deskripsi, harga, stok, kategori, aktif)
            VALUES (?, ?, ?, ?, ?, ?)
        """.trimIndent()

        return DatabaseOracle.gunakan { koneksi ->
            // Oracle: gunakan RETURNING ... INTO untuk mendapat ID
            val sqlDenganReturning = """
                INSERT INTO produk (nama, deskripsi, harga, stok, kategori, aktif)
                VALUES (?, ?, ?, ?, ?, ?)
                RETURNING id INTO ?
            """.trimIndent()

            koneksi.prepareCall(sqlDenganReturning).use { stmt ->
                stmt.setString(1, produk.nama)
                if (produk.deskripsi != null) stmt.setString(2, produk.deskripsi)
                else stmt.setNull(2, Types.CLOB)
                stmt.setBigDecimal(3, produk.harga)
                stmt.setInt(4, produk.stok)
                if (produk.kategori != null) stmt.setString(5, produk.kategori)
                else stmt.setNull(5, Types.VARCHAR)
                stmt.setInt(6, if (produk.aktif) 1 else 0)

                stmt.registerOutParameter(7, Types.NUMERIC)
                stmt.execute()

                val idBaru = stmt.getLong(7)
                produk.copy(id = idBaru)
            }
        }
    }

    // Alternatif INSERT dengan SEQUENCE.NEXTVAL (pre-12c)
    fun simpanDenganSequence(produk: Produk): Produk {
        val sql = """
            INSERT INTO produk (id, nama, harga, stok, kategori, aktif)
            VALUES (seq_produk.NEXTVAL, ?, ?, ?, ?, ?)
        """.trimIndent()

        return DatabaseOracle.gunakan { koneksi ->
            // Ambil ID yang akan digunakan dulu
            val idBaru = koneksi.createStatement().use { stmt ->
                stmt.executeQuery("SELECT seq_produk.NEXTVAL FROM DUAL").use { rs ->
                    rs.next(); rs.getLong(1)
                }
            }

            koneksi.prepareStatement(sql).use { stmt ->
                stmt.setString(1, produk.nama)
                stmt.setBigDecimal(2, produk.harga)
                stmt.setInt(3, produk.stok)
                if (produk.kategori != null) stmt.setString(4, produk.kategori)
                else stmt.setNull(4, Types.VARCHAR)
                stmt.setInt(5, if (produk.aktif) 1 else 0)
                stmt.executeUpdate()
            }

            produk.copy(id = idBaru)
        }
    }

    // SELECT dengan paginasi Oracle 12c+
    fun cariSemua(
        kategori: String? = null,
        halaman: Int = 1,
        ukuran: Int = 20
    ): List<Produk> {
        val offset = (halaman - 1) * ukuran
        val params = mutableListOf<Any?>()
        val kondisi = mutableListOf("aktif = 1")

        if (kategori != null) {
            kondisi.add("kategori = ?")
            params.add(kategori)
        }

        // Oracle 12c+: OFFSET...FETCH (sama dengan MSSQL)
        val sql = """
            SELECT id, nama, deskripsi, harga, stok, kategori, aktif
            FROM produk
            WHERE ${kondisi.joinToString(" AND ")}
            ORDER BY nama
            OFFSET ? ROWS FETCH NEXT ? ROWS ONLY
        """.trimIndent()

        return DatabaseOracle.gunakan { koneksi ->
            koneksi.prepareStatement(sql).use { stmt ->
                var idx = 1
                params.forEach { p ->
                    when (p) {
                        is String -> stmt.setString(idx++, p)
                        else -> stmt.setObject(idx++, p)
                    }
                }
                stmt.setInt(idx++, offset)
                stmt.setInt(idx, ukuran)

                stmt.executeQuery().use { rs ->
                    buildList { while (rs.next()) add(rs.toProduk()) }
                }
            }
        }
    }

    // SELECT dengan paginasi ROWNUM (Oracle lama, pre-12c)
    fun cariSemuaLama(kategori: String? = null, offset: Int = 0, limit: Int = 20): List<Produk> {
        val kondisiDalam = if (kategori != null) "AND kategori = ?" else ""
        val sql = """
            SELECT * FROM (
                SELECT t.*, ROWNUM AS rn FROM (
                    SELECT id, nama, deskripsi, harga, stok, kategori, aktif
                    FROM produk
                    WHERE aktif = 1 $kondisiDalam
                    ORDER BY nama
                ) t WHERE ROWNUM <= ?
            ) WHERE rn > ?
        """.trimIndent()

        return DatabaseOracle.gunakan { koneksi ->
            koneksi.prepareStatement(sql).use { stmt ->
                var idx = 1
                if (kategori != null) stmt.setString(idx++, kategori)
                stmt.setInt(idx++, offset + limit)  // batas atas
                stmt.setInt(idx, offset)             // batas bawah

                stmt.executeQuery().use { rs ->
                    buildList { while (rs.next()) add(rs.toProduk()) }
                }
            }
        }
    }
}

Stored Procedure PL/SQL #

PL/SQL adalah bahasa prosedural Oracle yang sangat powerful. Memanggil stored procedure Oracle dari Kotlin menggunakan CallableStatement:

-- Stored procedure Oracle (PL/SQL)
CREATE OR REPLACE PROCEDURE sp_tambah_stok (
    p_produk_id IN  NUMBER,
    p_jumlah    IN  NUMBER,
    p_stok_baru OUT NUMBER,
    p_status    OUT VARCHAR2
) AS
    v_stok_lama NUMBER;
BEGIN
    SELECT stok INTO v_stok_lama
    FROM produk
    WHERE id = p_produk_id
    FOR UPDATE;  -- kunci baris untuk update

    UPDATE produk
    SET stok = stok + p_jumlah
    WHERE id = p_produk_id;

    SELECT stok INTO p_stok_baru FROM produk WHERE id = p_produk_id;
    p_status := 'BERHASIL';

    COMMIT;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        ROLLBACK;
        p_stok_baru := -1;
        p_status := 'PRODUK_TIDAK_DITEMUKAN';
    WHEN OTHERS THEN
        ROLLBACK;
        p_stok_baru := -1;
        p_status := 'ERROR: ' || SQLERRM;
END sp_tambah_stok;
/
data class HasilTambahStok(val stokBaru: Int, val status: String)

fun tambahStokViaStoredProc(produkId: Long, jumlah: Int): HasilTambahStok {
    return DatabaseOracle.gunakan { koneksi ->
        // Sintaks Oracle: { call nama_procedure(?, ?, ?, ?) }
        koneksi.prepareCall("{ call sp_tambah_stok(?, ?, ?, ?) }").use { stmt ->
            // IN parameters
            stmt.setLong(1, produkId)
            stmt.setInt(2, jumlah)

            // OUT parameters — daftarkan tipenya
            stmt.registerOutParameter(3, Types.NUMERIC)  // p_stok_baru
            stmt.registerOutParameter(4, Types.VARCHAR)  // p_status

            stmt.execute()

            HasilTambahStok(
                stokBaru = stmt.getInt(3),
                status   = stmt.getString(4)
            )
        }
    }
}

// Penggunaan
fun main() {
    val hasil = tambahStokViaStoredProc(produkId = 1, jumlah = 50)
    when {
        hasil.status == "BERHASIL" -> println("Stok baru: ${hasil.stokBaru}")
        hasil.status == "PRODUK_TIDAK_DITEMUKAN" -> println("Produk tidak ditemukan")
        else -> println("Error: ${hasil.status}")
    }
}

Memanggil Function Oracle (bukan Procedure) #

Oracle juga punya FUNCTION yang mengembalikan nilai secara langsung:

CREATE OR REPLACE FUNCTION fn_harga_akhir (
    p_harga IN NUMBER,
    p_diskon IN NUMBER
) RETURN NUMBER AS
BEGIN
    RETURN p_harga * (1 - p_diskon / 100);
END fn_harga_akhir;
/
fun hitungHargaAkhir(harga: BigDecimal, diskon: Int): BigDecimal {
    return DatabaseOracle.gunakan { koneksi ->
        // Function Oracle: { ? = call nama_function(?, ?) }
        koneksi.prepareCall("{ ? = call fn_harga_akhir(?, ?) }").use { stmt ->
            stmt.registerOutParameter(1, Types.NUMERIC)  // return value
            stmt.setBigDecimal(2, harga)
            stmt.setInt(3, diskon)
            stmt.execute()
            stmt.getBigDecimal(1)
        }
    }
}

CLOB — Menangani Teks Besar #

Oracle menggunakan CLOB (Character Large Object) untuk teks yang melebihi 4000 karakter:

import oracle.jdbc.OracleTypes

// Tulis CLOB
fun simpanDenganClob(id: Long, teksLengkap: String) {
    DatabaseOracle.gunakan { koneksi ->
        koneksi.prepareStatement(
            "UPDATE produk SET deskripsi = ? WHERE id = ?"
        ).use { stmt ->
            // Untuk teks besar, gunakan setCharacterStream
            stmt.setCharacterStream(
                1,
                java.io.StringReader(teksLengkap),
                teksLengkap.length.toLong()
            )
            stmt.setLong(2, id)
            stmt.executeUpdate()
        }
    }
}

// Baca CLOB
fun bacaDeskripsi(id: Long): String? {
    return DatabaseOracle.gunakan { koneksi ->
        koneksi.prepareStatement(
            "SELECT deskripsi FROM produk WHERE id = ?"
        ).use { stmt ->
            stmt.setLong(1, id)
            stmt.executeQuery().use { rs ->
                if (!rs.next()) return@gunakan null
                val clob: Clob? = rs.getClob("deskripsi")
                clob?.let {
                    val isi = it.getSubString(1, it.length().toInt())
                    it.free()
                    isi
                }
            }
        }
    }
}

Exposed ORM dengan Oracle #

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

// Exposed mendukung Oracle dialect secara otomatis
fun inisialisasiExposedOracle() {
    Database.connect(DatabaseOracle.dataSource)
}

// Definisi tabel
object TabelProdukOracle : Table("PRODUK") {  // Oracle default UPPERCASE
    val id        = long("ID").autoIncrement()
    val nama      = varchar("NAMA", 255)
    val harga     = decimal("HARGA", 15, 2)
    val stok      = integer("STOK").default(0)
    val kategori  = varchar("KATEGORI", 100).nullable()
    val aktif     = integer("AKTIF").default(1)  // NUMBER(1) → Int

    override val primaryKey = PrimaryKey(id)
}

// CRUD dengan Exposed
fun tambahProduk(nama: String, harga: BigDecimal) = transaction {
    TabelProdukOracle.insertAndGetId {
        it[TabelProdukOracle.nama]  = nama
        it[TabelProdukOracle.harga] = harga
    }.value
}

fun cariProdukAktif() = transaction {
    TabelProdukOracle
        .select { TabelProdukOracle.aktif eq 1 }
        .orderBy(TabelProdukOracle.nama)
        .map { row ->
            Produk(
                id       = row[TabelProdukOracle.id],
                nama     = row[TabelProdukOracle.nama],
                harga    = row[TabelProdukOracle.harga],
                stok     = row[TabelProdukOracle.stok],
                kategori = row[TabelProdukOracle.kategori],
                aktif    = row[TabelProdukOracle.aktif] == 1
            )
        }
}
Oracle secara default menyimpan nama tabel dan kolom dalam HURUF BESAR jika tidak dikutip saat CREATE TABLE. Saat menggunakan Exposed atau query manual, pastikan nama tabel dan kolom menggunakan huruf besar yang konsisten, atau kutip nama yang case-sensitive dengan tanda kutip ganda: "Produk".

Flyway dengan Oracle #

import org.flywaydb.core.Flyway

fun jalankanMigrasiOracle() {
    val flyway = Flyway.configure()
        .dataSource(DatabaseOracle.dataSource)
        .locations("classpath:db/migration/oracle")
        .defaultSchema("MYAPP")              // schema Oracle (huruf besar)
        .baselineOnMigrate(true)
        .validateOnMigrate(true)
        .load()

    val hasil = flyway.migrate()
    println("Oracle Migration: ${hasil.migrationsExecuted} migrasi dijalankan")
}

File migrasi Oracle menggunakan PL/SQL:

-- V1__create_produk.sql
DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*) INTO v_count FROM user_tables WHERE table_name = 'PRODUK';
    IF v_count = 0 THEN
        EXECUTE IMMEDIATE '
            CREATE TABLE PRODUK (
                ID              NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
                NAMA            VARCHAR2(255) NOT NULL,
                DESKRIPSI       CLOB,
                HARGA           NUMBER(15,2) NOT NULL,
                STOK            NUMBER DEFAULT 0 NOT NULL,
                KATEGORI        VARCHAR2(100),
                AKTIF           NUMBER(1) DEFAULT 1 NOT NULL,
                DIBUAT_PADA     TIMESTAMP DEFAULT SYSTIMESTAMP
            )';
        EXECUTE IMMEDIATE 'CREATE INDEX IDX_PRODUK_KATEGORI ON PRODUK(KATEGORI)';
    END IF;
END;
/

Tips Khusus Oracle #

// 1. Selalu gunakan bind variables (PreparedStatement) — Oracle mengoptimalkan ini
// Dengan bind variable, Oracle bisa reuse execution plan

// 2. Waspadai string kosong = NULL
// Ini akan menyimpan NULL, bukan ""
stmt.setString(1, "")  // → NULL di Oracle

// Solusi: ubah string kosong ke null secara eksplisit
fun String?.toOracleString() = if (this.isNullOrEmpty()) null else this

// 3. Tanggal dan waktu Oracle
// Oracle punya TIMESTAMP WITH TIME ZONE untuk timezone-aware
val sqlTanggal = "SELECT TO_CHAR(SYSDATE, 'DD-MM-YYYY HH24:MI:SS') FROM DUAL"

// 4. Batch insert Oracle lebih optimal dengan array binding (Oracle-specific)
// Atau gunakan batch PreparedStatement biasa yang juga efisien

// 5. Untuk query yang sering dijalankan, pertimbangkan Statement Cache
val config = HikariConfig().apply {
    addDataSourceProperty("oracle.jdbc.implicitStatementCacheSize", "20")
}

Ringkasan #

  • GENERATED ALWAYS AS IDENTITY — gunakan ini (Oracle 12c+) sebagai pengganti AUTO_INCREMENT. Untuk Oracle lama, gunakan SEQUENCE dan masukkan seq.NEXTVAL di INSERT.
  • FROM DUAL — setiap SELECT tanpa tabel di Oracle butuh FROM DUAL. Contoh: SELECT SYSDATE FROM DUAL, SELECT 1 FROM DUAL untuk koneksi test.
  • String kosong = NULL — ini adalah gotcha terbesar Oracle. Di Oracle, VARCHAR2('') tersimpan sebagai NULL. Tangani ini dengan mengkonversi string kosong ke null sebelum insert.
  • VARCHAR2 bukan VARCHAR — Oracle merekomendasikan VARCHAR2 (bukan VARCHAR) untuk string. CLOB untuk teks yang mungkin melebihi 4000 karakter.
  • NUMBER(1) untuk boolean — Oracle tidak punya tipe boolean native. Gunakan NUMBER(1) dengan konvensi 0=false, 1=true, dan tambahkan CHECK constraint.
  • Paginasi OFFSET...FETCH — tersedia sejak Oracle 12c, sama dengan MSSQL. Untuk Oracle lama (pre-12c), gunakan subquery dengan ROWNUM.
  • RETURNING INTO untuk mendapat ID — Oracle tidak mendukung getGeneratedKeys() secara standar. Gunakan RETURNING id INTO ? dengan registerOutParameter untuk mendapat ID yang baru di-generate.
  • Nama tabel/kolom UPPERCASE — Oracle secara default case-insensitive dan menyimpan nama objek dalam huruf besar. Konsisten menggunakan huruf besar di kode Kotlin menghindari kebingungan.

← Sebelumnya: MSSQL   Berikutnya: PostgreSQL →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact