PostgreSQL

PostgreSQL #

PostgreSQL adalah database open source paling advanced di dunia — mendukung JSONB, array, full-text search, tipe geospasial, dan banyak fitur enterprise yang tidak ada di MySQL. Di ekosistem Dart, package postgres (versi 3+) menyediakan driver yang sangat matang dengan type-safe result, connection pooling, prepared statement, dan bahkan dukungan LISTEN/NOTIFY untuk notifikasi real-time. PostgreSQL adalah pilihan database yang sangat direkomendasikan untuk aplikasi Dart server-side.

Setup Package #

dart pub add postgres
# pubspec.yaml
dependencies:
  postgres: ^3.4.0

Koneksi ke PostgreSQL #

import 'package:postgres/postgres.dart';

Future<void> main() async {
  // Koneksi tunggal — untuk script CLI atau testing
  final conn = await Connection.open(
    Endpoint(
      host: 'localhost',
      port: 5432,
      database: 'toko_online',
      username: 'postgres',
      password: 'password',
    ),
    settings: ConnectionSettings(
      sslMode: SslMode.disable,     // atau SslMode.require untuk SSL
      connectTimeout: Duration(seconds: 10),
      queryTimeout: Duration(seconds: 30),
      applicationName: 'dart_app',  // terlihat di pg_stat_activity
    ),
  );

  print('Terhubung ke PostgreSQL');

  // Selalu tutup koneksi
  await conn.close();
}

Connection Pool — Untuk Server #

import 'package:postgres/postgres.dart';

// Singleton pool yang dibuat sekali dan digunakan di seluruh aplikasi
class DatabasePool {
  static Pool? _pool;

  static Pool dapatkan() {
    _pool ??= Pool.withEndpoints(
      [
        Endpoint(
          host: 'localhost',
          port: 5432,
          database: 'toko_online',
          username: 'appuser',
          password: 'password',
        ),
      ],
      settings: PoolSettings(
        maxConnectionCount: 10,       // maksimal koneksi simultan
        minConnectionCount: 2,        // minimal koneksi yang selalu siap
        connectTimeout: Duration(seconds: 5),
        queryTimeout: Duration(seconds: 30),
        applicationName: 'toko_server',
      ),
    );
    return _pool!;
  }

  static Future<void> tutup() async {
    await _pool?.close();
    _pool = null;
  }
}

// Penggunaan — pool secara otomatis mengelola siklus koneksi
Future<void> contohPakai() async {
  final pool = DatabasePool.dapatkan();

  // run() mengambil koneksi dari pool, jalankan callback, kembalikan otomatis
  final result = await pool.execute('SELECT COUNT(*) FROM produk WHERE aktif = true');
  print('Jumlah produk: ${result.first[0]}');
}

Query Dasar — Type-Safe Result #

Package postgres v3 mengembalikan hasil query yang type-safe melalui ResultRow:

import 'package:postgres/postgres.dart';

Future<void> contohQuery(Connection conn) async {
  // execute() — untuk query yang tidak perlu mapping kompleks
  final result = await conn.execute(
    'SELECT id, nama, email, dibuat_pada FROM pengguna ORDER BY nama',
  );

  for (final row in result) {
    // Akses via indeks
    final id = row[0] as int;
    final nama = row[1] as String;
    final email = row[2] as String?;
    final dibuatPada = row[3] as DateTime;

    print('$id: $nama ($email) — bergabung ${dibuatPada.year}');
  }

  // Akses via nama kolom (lebih ekspresif)
  for (final row in result) {
    final map = row.toColumnMap();
    print('${map['nama']}: ${map['email']}');
  }

  print('Total: ${result.numOfRows} baris');
}

Parameterized Query #

PostgreSQL menggunakan $1, $2, $3 sebagai placeholder parameter (positional):

import 'package:postgres/postgres.dart';

Future<void> contohParameterized(Connection conn) async {
  // ANTI-PATTERN: string interpolation — SQL injection!
  final inputBerbahaya = "'; DROP TABLE pengguna; --";
  await conn.execute(
    "SELECT * FROM pengguna WHERE nama = '$inputBerbahaya'",  // ✗ berbahaya!
  );

  // BENAR: parameterized dengan $1, $2, dst (positional)
  final result = await conn.execute(
    r'SELECT * FROM pengguna WHERE nama = $1 AND aktif = $2',
    parameters: ['Budi', true],
  );

  // Multiple parameter
  final produk = await conn.execute(
    r'SELECT * FROM produk WHERE kategori = $1 AND harga <= $2 AND aktif = true',
    parameters: ['elektronik', 5000000],
  );

  // LIKE
  final cari = 'laptop';
  final cariResult = await conn.execute(
    r'SELECT * FROM produk WHERE nama ILIKE $1',  // ILIKE = case-insensitive LIKE
    parameters: ['%$cari%'],
  );

  // Typed SQL — cara paling aman, tipe diverifikasi compile-time
  final typedResult = await conn.execute(
    Sql.indexed(
      r'SELECT id, nama, harga FROM produk WHERE id = $1',
    ),
    parameters: [42],
  );
}

CRUD Lengkap #

import 'package:postgres/postgres.dart';

class ProdukRepository {
  final Pool _pool;

  ProdukRepository(this._pool);

  // CREATE — RETURNING untuk dapatkan semua field yang baru diinsert
  Future<Map<String, dynamic>> tambah({
    required String nama,
    required double harga,
    required int stok,
    required String kategori,
  }) async {
    final result = await _pool.execute(
      r'INSERT INTO produk (nama, harga, stok, kategori, aktif, dibuat_pada) '
      r'VALUES ($1, $2, $3, $4, true, NOW()) '
      r'RETURNING *',
      parameters: [nama, harga, stok, kategori],
    );
    return result.first.toColumnMap();
  }

  // READ semua dengan pagination
  Future<List<Map<String, dynamic>>> ambilSemua({
    int halaman = 1,
    int perHalaman = 20,
    String? kategori,
    String urutan = 'dibuat_pada',
    bool desc = true,
  }) async {
    final offset = (halaman - 1) * perHalaman;
    final params = <Object?>[perHalaman, offset];
    var kondisi = 'WHERE aktif = true';

    if (kategori != null) {
      params.add(kategori);
      kondisi += ' AND kategori = \$${params.length}';
    }

    final arah = desc ? 'DESC' : 'ASC';
    final result = await _pool.execute(
      'SELECT id, nama, harga, stok, kategori, dibuat_pada '
      'FROM produk $kondisi '
      'ORDER BY $urutan $arah '
      r'LIMIT $1 OFFSET $2',
      parameters: params,
    );
    return result.map((row) => row.toColumnMap()).toList();
  }

  // READ satu
  Future<Map<String, dynamic>?> ambilById(int id) async {
    final result = await _pool.execute(
      r'SELECT * FROM produk WHERE id = $1 AND aktif = true',
      parameters: [id],
    );
    return result.isEmpty ? null : result.first.toColumnMap();
  }

  // UPDATE — hanya field yang diberikan
  Future<Map<String, dynamic>?> perbarui(int id, {
    String? nama,
    double? harga,
    int? stok,
  }) async {
    final sets = <String>[];
    final params = <Object?>[id];

    if (nama != null) {
      params.add(nama);
      sets.add('nama = \$${params.length}');
    }
    if (harga != null) {
      params.add(harga);
      sets.add('harga = \$${params.length}');
    }
    if (stok != null) {
      params.add(stok);
      sets.add('stok = \$${params.length}');
    }

    if (sets.isEmpty) return null;
    sets.add('diubah_pada = NOW()');

    final result = await _pool.execute(
      'UPDATE produk SET ${sets.join(', ')} WHERE id = \$1 AND aktif = true RETURNING *',
      parameters: params,
    );
    return result.isEmpty ? null : result.first.toColumnMap();
  }

  // DELETE (soft delete)
  Future<bool> hapus(int id) async {
    final result = await _pool.execute(
      r'UPDATE produk SET aktif = false, diubah_pada = NOW() WHERE id = $1',
      parameters: [id],
    );
    return result.affectedRows > 0;
  }

  // Hard delete — hati-hati!
  Future<bool> hapusPermanen(int id) async {
    final result = await _pool.execute(
      r'DELETE FROM produk WHERE id = $1',
      parameters: [id],
    );
    return result.affectedRows > 0;
  }
}

Mapping ke Model Kelas #

import 'package:postgres/postgres.dart';

class Produk {
  final int id;
  final String nama;
  final double harga;
  final int stok;
  final String kategori;
  final bool aktif;
  final DateTime dibuatPada;

  const Produk({
    required this.id,
    required this.nama,
    required this.harga,
    required this.stok,
    required this.kategori,
    required this.aktif,
    required this.dibuatPada,
  });

  // PostgreSQL mengembalikan tipe native langsung — tidak perlu parse
  factory Produk.dariMap(Map<String, dynamic> map) {
    return Produk(
      id: map['id'] as int,
      nama: map['nama'] as String,
      harga: (map['harga'] as num).toDouble(),
      stok: map['stok'] as int,
      kategori: map['kategori'] as String,
      aktif: map['aktif'] as bool,           // bool native dari PostgreSQL!
      dibuatPada: map['dibuat_pada'] as DateTime, // DateTime native dari PostgreSQL!
    );
  }
}

// Repository dengan typed model
Future<List<Produk>> ambilProdukAktif(Pool pool) async {
  final result = await pool.execute(
    'SELECT * FROM produk WHERE aktif = true ORDER BY nama',
  );
  return result.map((row) => Produk.dariMap(row.toColumnMap())).toList();
}

Transaksi #

import 'package:postgres/postgres.dart';

Future<int> buatOrder(Pool pool, int idPengguna,
    List<({int idProduk, int qty})> items) async {
  // runTx — transaksi dengan rollback otomatis saat exception
  return await pool.runTx((tx) async {
    // Buat record order
    final orderResult = await tx.execute(
      r'INSERT INTO orders (id_pengguna, status, total, dibuat_pada) '
      r'VALUES ($1, $2, 0, NOW()) RETURNING id',
      parameters: [idPengguna, 'pending'],
    );
    final idOrder = orderResult.first[0] as int;

    double totalHarga = 0;
    for (final item in items) {
      // Kunci baris dengan SELECT FOR UPDATE
      final stokResult = await tx.execute(
        r'SELECT stok, harga FROM produk WHERE id = $1 FOR UPDATE',
        parameters: [item.idProduk],
      );

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

      final stok = stokResult.first[0] as int;
      final harga = (stokResult.first[1] as num).toDouble();

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

      // Kurangi stok
      await tx.execute(
        r'UPDATE produk SET stok = stok - $1 WHERE id = $2',
        parameters: [item.qty, item.idProduk],
      );

      // Tambah item order
      final subtotal = harga * item.qty;
      await tx.execute(
        r'INSERT INTO order_items (id_order, id_produk, qty, harga, subtotal) '
        r'VALUES ($1, $2, $3, $4, $5)',
        parameters: [idOrder, item.idProduk, item.qty, harga, subtotal],
      );

      totalHarga += subtotal;
    }

    // Update total order
    await tx.execute(
      r'UPDATE orders SET total = $1 WHERE id = $2',
      parameters: [totalHarga, idOrder],
    );

    return idOrder;
    // runTx otomatis COMMIT jika tidak ada exception, ROLLBACK jika ada
  });
}

Tipe Data Khas PostgreSQL #

PostgreSQL memiliki tipe data yang sangat kaya — banyak di antaranya tidak ada di database lain:

JSONB — JSON yang Ter-index #

// INSERT dengan JSONB
await pool.execute(
  r'INSERT INTO log_aktivitas (id_pengguna, data, dibuat_pada) VALUES ($1, $2, NOW())',
  parameters: [
    42,
    // Map Dart di-convert ke JSON otomatis oleh driver postgres
    {'aksi': 'login', 'ip': '192.168.1.1', 'browser': 'Chrome'},
  ],
);

// Query dengan operator JSONB
final hasil = await pool.execute(
  r"SELECT * FROM log_aktivitas WHERE data->>'aksi' = $1",
  parameters: ['login'],
);

// Filter nested JSON
final filtered = await pool.execute(
  r"SELECT * FROM produk WHERE metadata @> $1::jsonb",
  parameters: ['{"warna": "merah"}'],  // @> = contains
);

Array #

// INSERT dengan array
await pool.execute(
  r'INSERT INTO produk (nama, tag) VALUES ($1, $2)',
  parameters: [
    'Laptop Gaming',
    ['gaming', 'laptop', 'electronics'],  // List Dart → PostgreSQL array
  ],
);

// Query dengan array operator
final hasilArray = await pool.execute(
  r"SELECT * FROM produk WHERE $1 = ANY(tag)",
  parameters: ['gaming'],  // cari produk yang punya tag 'gaming'
);

// Overlap: array yang memiliki elemen yang sama
final overlap = await pool.execute(
  r'SELECT * FROM produk WHERE tag && $1',
  parameters: [['gaming', 'laptop']],
);

UUID #

// PostgreSQL punya tipe UUID native
await pool.execute(
  r'INSERT INTO sesi (id, id_pengguna, dibuat_pada) VALUES (gen_random_uuid(), $1, NOW())',
  parameters: [42],
);

// Query dengan UUID
final sesi = await pool.execute(
  r'SELECT * FROM sesi WHERE id = $1::uuid',
  parameters: ['550e8400-e29b-41d4-a716-446655440000'],
);

COPY — Bulk Insert yang Sangat Cepat #

Untuk memasukkan ribuan hingga jutaan baris dengan sangat cepat, gunakan COPY — jauh lebih efisien dari INSERT satu per satu:

import 'package:postgres/postgres.dart';

Future<void> bulkInsert(Connection conn, List<Map<String, dynamic>> data) async {
  // COPY adalah perintah khusus PostgreSQL untuk bulk loading
  // Bisa 10-100x lebih cepat dari INSERT berulang

  // Metode 1: COPY FROM STDIN dengan string CSV
  await conn.execute('COPY produk (nama, harga, stok, kategori) FROM STDIN CSV');
  // Lanjutkan dengan data...

  // Metode 2: executeCopySql (lebih praktis)
  final csvData = data.map((row) =>
    '${row['nama']},${row['harga']},${row['stok']},${row['kategori']}'
  ).join('\n');

  await conn.runTx((tx) async {
    // Gunakan unnest untuk bulk insert dengan parameter
    final namas = data.map((r) => r['nama'] as String).toList();
    final hargas = data.map((r) => r['harga'] as double).toList();
    final stoks = data.map((r) => r['stok'] as int).toList();
    final kategoris = data.map((r) => r['kategori'] as String).toList();

    await tx.execute(
      r'INSERT INTO produk (nama, harga, stok, kategori) '
      r'SELECT * FROM unnest($1::text[], $2::numeric[], $3::int[], $4::text[])',
      parameters: [namas, hargas, stoks, kategoris],
    );
  });

  print('${data.length} baris berhasil diinsert');
}

LISTEN/NOTIFY — Notifikasi Real-Time #

PostgreSQL mendukung pub/sub sederhana melalui LISTEN dan NOTIFY — berguna untuk notifikasi antar proses atau real-time update:

import 'package:postgres/postgres.dart';

// Subscriber — dengarkan notifikasi
Future<void> dengarkan(Connection conn) async {
  // Subscribe ke channel
  await conn.execute('LISTEN perubahan_stok');
  await conn.execute('LISTEN order_baru');

  // Dengarkan notifikasi yang masuk
  conn.messages.listen((message) {
    if (message is ServerMessage) {
      print('Pesan dari server: $message');
    }
  });

  // atau gunakan notifications stream khusus
  await conn.execute('LISTEN test_channel');
  print('Mendengarkan notifikasi...');
  // Koneksi tetap aktif dan menunggu notifikasi
}

// Publisher — kirim notifikasi
Future<void> kirNotifikasi(Connection conn, String channel, String payload) async {
  await conn.execute(
    'SELECT pg_notify(\$1, \$2)',
    parameters: [channel, payload],
  );
  // Atau dengan NOTIFY langsung:
  // await conn.execute("NOTIFY $channel, '$payload'");
}

// Contoh penggunaan real-time
Future<void> contohRealTime() async {
  // Koneksi untuk subscriber (koneksi terpisah!)
  final connSub = await Connection.open(
    Endpoint(host: 'localhost', database: 'toko', username: 'app', password: 'pass'),
  );

  // Koneksi untuk publisher
  final connPub = await Connection.open(
    Endpoint(host: 'localhost', database: 'toko', username: 'app', password: 'pass'),
  );

  await connSub.execute('LISTEN stok_update');

  // Saat ada perubahan stok di aplikasi lain / trigger database:
  await connPub.execute(
    r"SELECT pg_notify('stok_update', $1)",
    parameters: ['{"produk_id": 42, "stok_baru": 5}'],
  );

  await connSub.close();
  await connPub.close();
}

Upsert — INSERT atau UPDATE #

PostgreSQL mendukung INSERT ... ON CONFLICT yang sangat powerful:

import 'package:postgres/postgres.dart';

Future<void> contohUpsert(Pool pool) async {
  // Upsert — insert jika belum ada, update jika sudah ada
  await pool.execute(
    r'INSERT INTO konfigurasi (kunci, nilai, diubah_pada) '
    r'VALUES ($1, $2, NOW()) '
    r'ON CONFLICT (kunci) DO UPDATE SET '
    r'nilai = EXCLUDED.nilai, diubah_pada = NOW()',
    parameters: ['tema', 'gelap'],
  );

  // INSERT yang tidak melakukan apapun jika sudah ada (ignore duplicate)
  await pool.execute(
    r'INSERT INTO tag_produk (id_produk, tag) VALUES ($1, $2) '
    r'ON CONFLICT (id_produk, tag) DO NOTHING',
    parameters: [42, 'gaming'],
  );

  // Update dengan kondisi — hanya update jika nilai baru berbeda
  await pool.execute(
    r'INSERT INTO cache (kunci, nilai, kadaluarsa) VALUES ($1, $2, NOW() + INTERVAL $3) '
    r'ON CONFLICT (kunci) DO UPDATE SET '
    r'nilai = EXCLUDED.nilai, '
    r'kadaluarsa = EXCLUDED.kadaluarsa '
    r'WHERE cache.nilai IS DISTINCT FROM EXCLUDED.nilai',
    parameters: ['user_count', '42', '1 hour'],
  );
}

Anti-Pattern PostgreSQL di Dart #

Tidak Menggunakan Pool di Server #

// ANTI-PATTERN: buat koneksi baru setiap request
Future<Response> handler(Request req) async {
  final conn = await Connection.open(Endpoint(/* ... */)); // ✗ lambat!
  final data = await conn.execute('SELECT ...');
  await conn.close();
  return Response.ok(data);
}

// BENAR: gunakan Pool singleton
final _pool = Pool.withEndpoints([Endpoint(/* ... */)],
    settings: PoolSettings(maxConnectionCount: 10));

Future<Response> handler(Request req) async {
  final data = await _pool.execute('SELECT ...'); // ✓ cepat
  return Response.ok(data);
}

SELECT * tanpa Kebutuhan #

// ANTI-PATTERN: SELECT * — ambil semua kolom meski tidak semua dibutuhkan
final result = await pool.execute('SELECT * FROM produk'); // ✗ buang bandwidth dan memori

// BENAR: pilih hanya kolom yang dibutuhkan
final result = await pool.execute(
  'SELECT id, nama, harga FROM produk WHERE aktif = true',
);

N+1 Query — Sama seperti Database Lain #

// ANTI-PATTERN: query per item di dalam loop
Future<void> buruk(Pool pool) async {
  final orders = await pool.execute('SELECT id FROM orders WHERE status = $1',
      parameters: ['pending']);

  for (final order in orders) {
    final id = order[0] as int;
    final items = await pool.execute( // ✗ N+1!
      r'SELECT * FROM order_items WHERE id_order = $1',
      parameters: [id],
    );
  }
}

// BENAR: satu query dengan JOIN
Future<void> baik(Pool pool) async {
  final result = await pool.execute(
    'SELECT o.id, o.status, oi.id_produk, oi.qty, oi.subtotal '
    'FROM orders o '
    'JOIN order_items oi ON oi.id_order = o.id '
    'WHERE o.status = $1 '
    'ORDER BY o.id',
    parameters: ['pending'],
  );
  // kelompokkan di Dart
}

Ringkasan #

  • Package postgres v3 adalah driver PostgreSQL Dart terbaik — mendukung typed result, connection pool, transaksi, prepared statement, dan LISTEN/NOTIFY.
  • Placeholder positional $1, $2, $3 — berbeda dari MySQL (:nama) dan MSSQL (@nama). Selalu gunakan parameterized query, jangan interpolasi string.
  • RETURNING * setelah INSERT/UPDATE mengembalikan row yang terpengaruh — sangat berguna untuk mendapatkan semua field (termasuk yang diset database) setelah operasi.
  • Tipe native PostgreSQL — driver mengembalikan int, double, bool, DateTime, List secara langsung tanpa perlu parsing manual seperti di MySQL.
  • pool.runTx((tx) async {...}) menangani COMMIT dan ROLLBACK secara otomatis — jauh lebih aman dari BEGIN/COMMIT manual.
  • JSONB memungkinkan menyimpan JSON di PostgreSQL yang bisa di-query dan di-index — ideal untuk data semi-structured yang fleksibel.
  • Array PostgreSQL bisa diisi langsung dari List Dart dan di-query dengan operator ANY, ALL, && (overlap), @> (contains).
  • unnest($1::text[], $2::int[]) untuk bulk insert yang type-safe dan efisien — lebih baik dari executeCopy untuk kebanyakan kasus.
  • LISTEN/NOTIFY memungkinkan pub/sub antar koneksi database — berguna untuk trigger real-time update tanpa polling.
  • ON CONFLICT DO UPDATE (upsert) dan ON CONFLICT DO NOTHING menggantikan INSERT IGNORE dan ON DUPLICATE KEY UPDATE MySQL dengan sintaks yang lebih ekspresif.

← Sebelumnya: Oracle   Berikutnya: MongoDB →

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