Oracle

Oracle #

Oracle Database adalah sistem manajemen basis data enterprise paling mature di dunia — digunakan oleh bank, pemerintah, dan perusahaan Fortune 500 di seluruh dunia. Berbeda dari MySQL dan PostgreSQL yang memiliki driver native Dart yang matang, koneksi Dart ke Oracle memiliki dua jalur utama: melalui Oracle REST Data Services (ORDS) menggunakan HTTP client biasa, atau menggunakan package dart_oracle yang membutuhkan Oracle Instant Client. Artikel ini membahas keduanya beserta sintaks PL/SQL yang khas Oracle yang perlu dipahami.

Dua Pendekatan Koneksi Oracle #

flowchart TD
    A[Aplikasi Dart] --> B{Strategi koneksi?}
    B -- Infrastruktur\nsudah ada ORDS --> C[Oracle REST Data Services\nmelalui http package]
    B -- Akses langsung\nke database --> D[dart_oracle package\n+ Oracle Instant Client]
    C --> E[REST API / JSON\nCross-platform\nTidak perlu native lib]
    D --> F[SQL langsung\nPerforma lebih baik\nButuh Instant Client di mesin]
Pendekatan Kelebihan Kekurangan
Via ORDS Cross-platform, tidak butuh native lib, bisa via firewall Butuh konfigurasi ORDS di server Oracle
dart_oracle langsung Query SQL langsung, performa lebih baik Butuh Oracle Instant Client di mesin Dart

Pendekatan 1: Via Oracle REST Data Services (ORDS) #

ORDS adalah layer REST API yang disediakan Oracle yang mengekspos database via HTTP/HTTPS. Ini adalah pendekatan yang paling portable untuk Dart karena hanya membutuhkan http package.

dart pub add http
dart pub add dart_jsonwebtoken  # untuk autentikasi JWT jika diperlukan
import 'dart:convert';
import 'package:http/http.dart' as http;

class OracleORDSClient {
  final String baseUrl;
  final String schema;
  String? _token;

  OracleORDSClient({
    required this.baseUrl,    // contoh: 'https://oracle-server:8443/ords'
    required this.schema,     // nama schema/workspace ORDS
  });

  // Login dan dapatkan token OAuth2
  Future<void> autentikasi(String clientId, String clientSecret) async {
    final response = await http.post(
      Uri.parse('$baseUrl/oauth/token'),
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Basic ${base64Encode(utf8.encode('$clientId:$clientSecret'))}',
      },
      body: 'grant_type=client_credentials',
    );

    if (response.statusCode != 200) {
      throw Exception('Autentikasi gagal: ${response.body}');
    }

    final data = jsonDecode(response.body);
    _token = data['access_token'] as String;
  }

  Map<String, String> get _headers => {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $_token',
  };

  // Eksekusi query via ORDS REST endpoint
  Future<List<Map<String, dynamic>>> query(
    String endpoint, {
    Map<String, String>? params,
  }) async {
    var uri = Uri.parse('$baseUrl/$schema/$endpoint');
    if (params != null) {
      uri = uri.replace(queryParameters: params);
    }

    final response = await http.get(uri, headers: _headers);

    if (response.statusCode != 200) {
      throw Exception('Query gagal (${response.statusCode}): ${response.body}');
    }

    final data = jsonDecode(response.body) as Map<String, dynamic>;
    // ORDS mengembalikan data dalam field 'items'
    return (data['items'] as List? ?? []).cast<Map<String, dynamic>>();
  }

  // POST untuk insert/update via ORDS AutoREST
  Future<Map<String, dynamic>> post(
    String endpoint,
    Map<String, dynamic> body,
  ) async {
    final response = await http.post(
      Uri.parse('$baseUrl/$schema/$endpoint'),
      headers: _headers,
      body: jsonEncode(body),
    );

    if (response.statusCode != 200 && response.statusCode != 201) {
      throw Exception('POST gagal (${response.statusCode}): ${response.body}');
    }

    return jsonDecode(response.body) as Map<String, dynamic>;
  }
}

// Penggunaan
Future<void> main() async {
  final client = OracleORDSClient(
    baseUrl: 'https://oracle-server:8443/ords',
    schema: 'toko',
  );

  await client.autentikasi('myapp_client_id', 'myapp_client_secret');

  // ORDS AutoREST: GET /ords/toko/produk/
  final produk = await client.query('produk', params: {'limit': '10'});
  for (final p in produk) {
    print('${p['NAMA']}: ${p['HARGA']}');
  }
}

Pendekatan 2: dart_oracle — Koneksi Langsung #

Untuk koneksi langsung tanpa ORDS, gunakan package dart_oracle yang membutuhkan Oracle Instant Client:

dart pub add dart_oracle

Prasyarat: Oracle Instant Client #

# macOS — via Homebrew
brew install oracle-instant-client

# Linux
# Download dari https://www.oracle.com/database/technologies/instant-client/downloads.html
# Ekstrak dan set LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/path/to/instant_client:$LD_LIBRARY_PATH

# Windows
# Download dan tambahkan folder ke PATH
import 'package:dart_oracle/dart_oracle.dart';

Future<void> main() async {
  // Format connection string Oracle:
  // host:port/service_name  (untuk Oracle 12c+)
  // atau //host:port/service_name (ezconnect format)
  final conn = await OracleConnection.connect(
    connectionString: 'localhost:1521/ORCLPDB1', // atau ORCL untuk CDB
    username: 'SCOTT',
    password: 'tiger',
  );

  print('Terhubung ke Oracle Database');

  await conn.close();
}

String Koneksi Oracle #

// Beberapa format string koneksi Oracle yang umum:

// EZConnect (paling sederhana)
'localhost:1521/ORCLPDB1'

// EZConnect dengan service name eksplisit
'(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCLPDB1)))'

// TNS alias (jika tnsnames.ora dikonfigurasi)
'MYDB'

// RAC / failover
'(DESCRIPTION=(FAILOVER=ON)(LOAD_BALANCE=OFF)'
'(ADDRESS_LIST='
'  (ADDRESS=(PROTOCOL=TCP)(HOST=rac1)(PORT=1521))'
'  (ADDRESS=(PROTOCOL=TCP)(HOST=rac2)(PORT=1521)))'
'(CONNECT_DATA=(SERVICE_NAME=MYSERVICE)))'

Sintaks SQL Oracle — Perbedaan Krusial #

Oracle menggunakan dialek SQL yang sangat berbeda dari MySQL dan PostgreSQL. Ini adalah sumber kebingungan terbesar bagi developer yang pindah dari database lain:

-- MySQL / PostgreSQL        | Oracle (PL/SQL)
-- -------------------------|--------------------------------
-- AUTO_INCREMENT            | SEQUENCE + TRIGGER atau GENERATED AS IDENTITY (12c+)
-- LIMIT n                   | ROWNUM <= n (lama) atau FETCH FIRST n ROWS ONLY (12c+)
-- NOW() atau CURRENT_TIMESTAMP | SYSDATE atau SYSTIMESTAMP
-- CONCAT(a, b)              | a || b
-- IFNULL(x, y)             | NVL(x, y) atau COALESCE(x, y)
-- TRUE / FALSE              | 1 / 0 (tidak ada tipe BOOLEAN di SQL Oracle)
-- SHOW TABLES               | SELECT table_name FROM user_tables
-- SELECT 1                  | SELECT 1 FROM DUAL
-- information_schema        | data_dictionary views (user_tables, all_columns, dll.)
-- VARCHAR                   | VARCHAR2 (gunakan ini, bukan VARCHAR)
// Contoh sintaks Oracle yang perlu diperhatikan

// 1. Pagination Oracle 12c+ (direkomendasikan)
final paginasi12c = await conn.execute(
  'SELECT id, nama, harga FROM produk '
  'ORDER BY dibuat_pada DESC '
  'OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY',
  {'offset': 0, 'limit': 20},
);

// 2. Pagination Oracle 11g dan sebelumnya (ROWNUM)
final paginasiLama = await conn.execute(
  'SELECT * FROM ( '
  '  SELECT a.*, ROWNUM rnum FROM ( '
  '    SELECT id, nama, harga FROM produk ORDER BY dibuat_pada DESC '
  '  ) a WHERE ROWNUM <= :limit '
  ') WHERE rnum > :offset',
  {'limit': 20, 'offset': 0},
);

// 3. INSERT dengan SEQUENCE (Oracle tradisional)
// Perlu buat sequence dulu: CREATE SEQUENCE seq_produk START WITH 1 INCREMENT BY 1
await conn.execute(
  'INSERT INTO produk (id, nama, harga, dibuat_pada) '
  'VALUES (seq_produk.NEXTVAL, :nama, :harga, SYSDATE)',
  {'nama': 'Laptop', 'harga': 15000000},
);

// 4. INSERT dengan GENERATED AS IDENTITY (Oracle 12c+)
await conn.execute(
  'INSERT INTO produk (nama, harga, dibuat_pada) VALUES (:nama, :harga, SYSDATE)',
  {'nama': 'Mouse', 'harga': 250000},
);

// 5. Query ke tabel dual (untuk fungsi tanpa tabel)
final tanggal = await conn.execute('SELECT SYSDATE AS sekarang FROM DUAL');

CRUD dengan dart_oracle #

import 'package:dart_oracle/dart_oracle.dart';
import 'dart:convert';

class ProdukRepository {
  final OracleConnection _conn;

  ProdukRepository(this._conn);

  // CREATE — dengan RETURNING untuk dapatkan ID
  Future<int> tambah({
    required String nama,
    required double harga,
    required int stok,
  }) async {
    // RETURNING ... INTO untuk mendapatkan nilai yang baru diinsert
    final result = await _conn.execute(
      'INSERT INTO produk (id, nama, harga, stok, aktif, dibuat_pada) '
      'VALUES (seq_produk.NEXTVAL, :nama, :harga, :stok, 1, SYSDATE) '
      'RETURNING id INTO :id_baru',
      {'nama': nama, 'harga': harga, 'stok': stok},
      outParams: {'id_baru': OracleType.number},
    );
    return result.outValues['id_baru'] as int;
  }

  // READ semua
  Future<List<Map<String, dynamic>>> ambilSemua({
    int halaman = 1,
    int perHalaman = 20,
  }) async {
    final offset = (halaman - 1) * perHalaman;
    final result = await _conn.query(
      'SELECT id, nama, harga, stok '
      'FROM produk '
      'WHERE aktif = 1 '
      'ORDER BY nama '
      'OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY',
      {'offset': offset, 'limit': perHalaman},
    );
    return result.rows.map((row) => row.toMap()).toList();
  }

  // READ satu
  Future<Map<String, dynamic>?> ambilById(int id) async {
    final result = await _conn.query(
      'SELECT * FROM produk WHERE id = :id AND aktif = 1',
      {'id': id},
    );
    return result.rows.isEmpty ? null : result.rows.first.toMap();
  }

  // UPDATE
  Future<bool> perbarui(int id, {String? nama, double? harga, int? stok}) async {
    final fields = <String>[];
    final params = <String, dynamic>{'id': id};

    if (nama != null) { fields.add('nama = :nama'); params['nama'] = nama; }
    if (harga != null) { fields.add('harga = :harga'); params['harga'] = harga; }
    if (stok != null) { fields.add('stok = :stok'); params['stok'] = stok; }
    if (fields.isEmpty) return false;

    // Oracle: SYSDATE sebagai timestamp
    fields.add('diubah_pada = SYSDATE');

    final result = await _conn.execute(
      'UPDATE produk SET ${fields.join(', ')} WHERE id = :id',
      params,
    );
    return result.rowsAffected > 0;
  }

  // DELETE soft
  Future<bool> hapus(int id) async {
    final result = await _conn.execute(
      'UPDATE produk SET aktif = 0, diubah_pada = SYSDATE WHERE id = :id',
      {'id': id},
    );
    return result.rowsAffected > 0;
  }
}

Transaksi Oracle #

import 'package:dart_oracle/dart_oracle.dart';

Future<void> buatOrder(
  OracleConnection conn,
  int idPengguna,
  List<({int idProduk, int qty})> items,
) async {
  // Oracle tidak perlu BEGIN TRANSACTION eksplisit
  // Setiap sesi dimulai dalam mode auto-commit OFF secara default
  // Perlu COMMIT atau ROLLBACK secara eksplisit

  try {
    // Buat order
    final orderResult = await conn.execute(
      'INSERT INTO orders (id, id_pengguna, status, dibuat_pada) '
      'VALUES (seq_order.NEXTVAL, :uid, :status, SYSDATE) '
      'RETURNING id INTO :id_baru',
      {'uid': idPengguna, 'status': 'PENDING'},
      outParams: {'id_baru': OracleType.number},
    );
    final idOrder = orderResult.outValues['id_baru'] as int;

    double totalHarga = 0;
    for (final item in items) {
      // Cek dan kunci stok dengan SELECT FOR UPDATE
      final stokResult = await conn.query(
        'SELECT stok, harga FROM produk WHERE id = :id FOR UPDATE',
        {'id': item.idProduk},
      );

      if (stokResult.rows.isEmpty) {
        throw Exception('Produk ${item.idProduk} tidak ditemukan');
      }

      final row = stokResult.rows.first.toMap();
      final stok = row['STOK'] as int;
      final harga = (row['HARGA'] as num).toDouble();

      if (stok < item.qty) {
        throw Exception('Stok tidak mencukupi untuk produk ${item.idProduk}');
      }

      // Update stok
      await conn.execute(
        'UPDATE produk SET stok = stok - :qty WHERE id = :id',
        {'qty': item.qty, 'id': item.idProduk},
      );

      // Insert item order
      final subtotal = harga * item.qty;
      await conn.execute(
        'INSERT INTO order_item (id, id_order, id_produk, qty, harga, subtotal) '
        'VALUES (seq_order_item.NEXTVAL, :oid, :pid, :qty, :harga, :subtotal)',
        {
          'oid': idOrder, 'pid': item.idProduk,
          'qty': item.qty, 'harga': harga, 'subtotal': subtotal,
        },
      );

      totalHarga += subtotal;
    }

    // Update total
    await conn.execute(
      'UPDATE orders SET total = :total WHERE id = :id',
      {'total': totalHarga, 'id': idOrder},
    );

    // COMMIT — Oracle tidak auto-commit secara default
    await conn.commit();
    print('Order #$idOrder berhasil, total: Rp${totalHarga.toStringAsFixed(0)}');

  } catch (e) {
    // ROLLBACK semua perubahan
    await conn.rollback();
    print('Order gagal, rollback: $e');
    rethrow;
  }
}

Stored Procedure dan PL/SQL #

Oracle sangat kaya fitur stored procedure melalui PL/SQL:

import 'package:dart_oracle/dart_oracle.dart';

// Panggil stored procedure Oracle
Future<void> panggilStoredProcedure(OracleConnection conn) async {
  // Stored procedure dengan IN dan OUT parameter
  // Asumsi di Oracle:
  // CREATE OR REPLACE PROCEDURE sp_hitung_diskon(
  //   p_harga IN NUMBER,
  //   p_level IN VARCHAR2,
  //   p_diskon OUT NUMBER
  // ) AS BEGIN
  //   IF p_level = 'PREMIUM' THEN p_diskon := p_harga * 0.2;
  //   ELSE p_diskon := p_harga * 0.1; END IF;
  // END;

  final result = await conn.execute(
    'BEGIN sp_hitung_diskon(:p_harga, :p_level, :p_diskon); END;',
    {'p_harga': 150000, 'p_level': 'PREMIUM'},
    outParams: {'p_diskon': OracleType.number},
  );

  final diskon = result.outValues['p_diskon'] as double;
  print('Diskon: Rp${diskon.toStringAsFixed(0)}');
}

// Panggil function Oracle (mengembalikan nilai)
Future<double> panggilFunction(OracleConnection conn, int idProduk) async {
  // Oracle function bisa dipanggil dalam SELECT
  final result = await conn.query(
    'SELECT fn_harga_setelah_diskon(:id_produk) AS harga_diskon FROM DUAL',
    {'id_produk': idProduk},
  );
  return (result.rows.first.toMap()['HARGA_DISKON'] as num).toDouble();
}

// Eksekusi PL/SQL anonim
Future<void> plsqlAnonim(OracleConnection conn) async {
  // Blok PL/SQL anonim untuk logika yang kompleks
  await conn.execute('''
    DECLARE
      v_stok NUMBER;
      v_min_stok CONSTANT NUMBER := 10;
    BEGIN
      SELECT stok INTO v_stok FROM produk WHERE id = :id;

      IF v_stok < v_min_stok THEN
        INSERT INTO notifikasi (pesan, dibuat_pada)
        VALUES ('Stok produk ' || :id || ' hampir habis: ' || v_stok, SYSDATE);
        COMMIT;
      END IF;
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        NULL; -- produk tidak ada, abaikan
    END;
  ''', {'id': 42});
}

Perbandingan Tiga Database SQL di Dart #

Fitur MySQL MSSQL Oracle
Package mysql_client mssql_connection dart_oracle / via ORDS
Placeholder :nama @nama :nama
Auto-increment AUTO_INCREMENT IDENTITY(1,1) SEQUENCE + trigger
Top N rows LIMIT n TOP n FETCH FIRST n ROWS ONLY
Tanggal sekarang NOW() GETDATE() SYSDATE
Null check IFNULL() ISNULL() NVL()
String concat CONCAT() atau ` `
Boolean BOOLEAN / TINYINT BIT Tidak ada (gunakan NUMBER(1))
Transaksi transactional() BEGIN/COMMIT manual COMMIT/ROLLBACK eksplisit
Dummy table Tidak perlu Tidak perlu FROM DUAL wajib

Ringkasan #

  • Dua jalur koneksi Oracle — via ORDS (HTTP, cross-platform, tidak perlu native lib) atau via dart_oracle (SQL langsung, performa lebih baik, butuh Oracle Instant Client).
  • String koneksi Oracle menggunakan format host:port/service_name — berbeda dari host:port/database MySQL. Service name bisa ditemukan dari SHOW PARAMETER SERVICE_NAME di SQL*Plus.
  • Tidak ada AUTO_INCREMENT — Oracle menggunakan SEQUENCE untuk generate nilai unik. Oracle 12c+ mendukung GENERATED AS IDENTITY sebagai shortcut.
  • RETURNING id INTO :out_param adalah cara Oracle mendapatkan ID yang baru diinsert — pengganti lastInsertID dari MySQL.
  • Pagination Oracle lama (≤11g): nested ROWNUM query. Oracle modern (12c+): OFFSET n ROWS FETCH NEXT m ROWS ONLY — mirip dengan SQL standar.
  • Oracle tidak auto-commit — setiap sesi dimulai dengan auto-commit OFF. Selalu panggil commit() setelah perubahan yang berhasil, atau rollback() saat gagal.
  • FROM DUAL wajib untuk query tanpa tabel (misal: SELECT SYSDATE FROM DUAL) — tabel dummy satu baris satu kolom milik Oracle.
  • Gunakan VARCHAR2, bukan VARCHAR di Oracle — meski keduanya valid, VARCHAR2 adalah tipe yang direkomendasikan Oracle untuk string.
  • Nama kolom dari Oracle selalu UPPERCASE (kecuali dibuat dengan tanda kutip ganda) — akses dengan row['NAMA'] bukan row['nama'].
  • PL/SQL anonim dan stored procedure sangat umum di Oracle enterprise — pelajari sintaks BEGIN...END, EXCEPTION WHEN, dan OUT parameter.

← Sebelumnya: MSSQL   Berikutnya: PostgreSQL →

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