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
postgresv3 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,Listsecara 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
ListDart dan di-query dengan operatorANY,ALL,&&(overlap),@>(contains).unnest($1::text[], $2::int[])untuk bulk insert yang type-safe dan efisien — lebih baik dariexecuteCopyuntuk kebanyakan kasus.LISTEN/NOTIFYmemungkinkan pub/sub antar koneksi database — berguna untuk trigger real-time update tanpa polling.ON CONFLICT DO UPDATE(upsert) danON CONFLICT DO NOTHINGmenggantikanINSERT IGNOREdanON DUPLICATE KEY UPDATEMySQL dengan sintaks yang lebih ekspresif.