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 penggantiAUTO_INCREMENT. Untuk Oracle lama, gunakanSEQUENCEdan masukkanseq.NEXTVALdi INSERT.FROM DUAL— setiap SELECT tanpa tabel di Oracle butuhFROM DUAL. Contoh:SELECT SYSDATE FROM DUAL,SELECT 1 FROM DUALuntuk koneksi test.- String kosong = NULL — ini adalah gotcha terbesar Oracle. Di Oracle,
VARCHAR2('')tersimpan sebagaiNULL. Tangani ini dengan mengkonversi string kosong ke null sebelum insert.VARCHAR2bukanVARCHAR— Oracle merekomendasikanVARCHAR2(bukanVARCHAR) untuk string.CLOBuntuk teks yang mungkin melebihi 4000 karakter.NUMBER(1)untuk boolean — Oracle tidak punya tipe boolean native. GunakanNUMBER(1)dengan konvensi 0=false, 1=true, dan tambahkanCHECKconstraint.- Paginasi
OFFSET...FETCH— tersedia sejak Oracle 12c, sama dengan MSSQL. Untuk Oracle lama (pre-12c), gunakan subquery denganROWNUM.RETURNING INTOuntuk mendapat ID — Oracle tidak mendukunggetGeneratedKeys()secara standar. GunakanRETURNING id INTO ?denganregisterOutParameteruntuk 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.