PostgreSQL #
PostgreSQL is the most advanced open-source database in the world — supporting JSONB, arrays, full-text search, geospatial types, and many enterprise features that MySQL doesn’t have. In the Dart ecosystem, the postgres package (version 3+) provides a very mature driver with type-safe results, connection pooling, prepared statements, and even LISTEN/NOTIFY support for real-time notifications. PostgreSQL is a highly recommended database choice for Dart server-side applications.
Package Setup #
dart pub add postgres
# pubspec.yaml
dependencies:
postgres: ^3.4.0
Connecting to PostgreSQL #
import 'package:postgres/postgres.dart';
Future<void> main() async {
// Single connection — for CLI scripts or testing
final conn = await Connection.open(
Endpoint(
host: 'localhost',
port: 5432,
database: 'toko_online',
username: 'postgres',
password: 'password',
),
settings: ConnectionSettings(
sslMode: SslMode.disable, // or SslMode.require for SSL
connectTimeout: Duration(seconds: 10),
queryTimeout: Duration(seconds: 30),
applicationName: 'dart_app', // visible in pg_stat_activity
),
);
print('Connected to PostgreSQL');
// Always close the connection
await conn.close();
}
Connection Pool — For Servers #
import 'package:postgres/postgres.dart';
// Singleton pool created once and used throughout the app
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, // maximum simultaneous connections
minConnectionCount: 2, // minimum connections always ready
connectTimeout: Duration(seconds: 5),
queryTimeout: Duration(seconds: 30),
applicationName: 'toko_server',
),
);
return _pool!;
}
static Future<void> tutup() async {
await _pool?.close();
_pool = null;
}
}
// Usage — the pool automatically manages the connection lifecycle
Future<void> contohPakai() async {
final pool = DatabasePool.dapatkan();
// run() takes a connection from the pool, runs the callback, returns it automatically
final result = await pool.execute('SELECT COUNT(*) FROM produk WHERE aktif = true');
print('Product count: ${result.first[0]}');
}
Basic Queries — Type-Safe Results #
The postgres v3 package returns type-safe query results through ResultRow:
import 'package:postgres/postgres.dart';
Future<void> contohQuery(Connection conn) async {
// execute() — for queries that don't need complex mapping
final result = await conn.execute(
'SELECT id, nama, email, dibuat_pada FROM pengguna ORDER BY nama',
);
for (final row in result) {
// Access by index
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) — joined ${dibuatPada.year}');
}
// Access by column name (more expressive)
for (final row in result) {
final map = row.toColumnMap();
print('${map['nama']}: ${map['email']}');
}
print('Total: ${result.numOfRows} rows');
}
Parameterized Queries #
PostgreSQL uses $1, $2, $3 as parameter placeholders (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'", // ✗ dangerous!
);
// CORRECT: parameterized with $1, $2, etc. (positional)
final result = await conn.execute(
r'SELECT * FROM pengguna WHERE nama = $1 AND aktif = $2',
parameters: ['Budi', true],
);
// Multiple parameters
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 — the safest way, types verified at compile time
final typedResult = await conn.execute(
Sql.indexed(
r'SELECT id, nama, harga FROM produk WHERE id = $1',
),
parameters: [42],
);
}
Full CRUD #
import 'package:postgres/postgres.dart';
class ProdukRepository {
final Pool _pool;
ProdukRepository(this._pool);
// CREATE — RETURNING to get all newly inserted fields
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 all with 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 one
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 — only the fields given
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 — be careful!
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 to Model Classes #
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 returns native types directly — no parsing needed
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, // native bool from PostgreSQL!
dibuatPada: map['dibuat_pada'] as DateTime, // native DateTime from PostgreSQL!
);
}
}
// Repository with a 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();
}
Transactions #
import 'package:postgres/postgres.dart';
Future<int> buatOrder(Pool pool, int idPengguna,
List<({int idProduk, int qty})> items) async {
// runTx — a transaction with automatic rollback on exceptions
return await pool.runTx((tx) async {
// Create the order record
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) {
// Lock the row with 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('Product ${item.idProduk} not found');
}
final stok = stokResult.first[0] as int;
final harga = (stokResult.first[1] as num).toDouble();
if (stok < item.qty) {
throw Exception('Insufficient stock for product ${item.idProduk}');
}
// Decrease stock
await tx.execute(
r'UPDATE produk SET stok = stok - $1 WHERE id = $2',
parameters: [item.qty, item.idProduk],
);
// Add the order item
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 the order total
await tx.execute(
r'UPDATE orders SET total = $1 WHERE id = $2',
parameters: [totalHarga, idOrder],
);
return idOrder;
// runTx automatically COMMITs if there's no exception, ROLLBACKs if there is
});
}
Distinctive PostgreSQL Data Types #
PostgreSQL has a very rich set of data types — many of which don’t exist in other databases:
JSONB — Indexed JSON #
// INSERT with JSONB
await pool.execute(
r'INSERT INTO log_aktivitas (id_pengguna, data, dibuat_pada) VALUES ($1, $2, NOW())',
parameters: [
42,
// Dart Maps are converted to JSON automatically by the postgres driver
{'aksi': 'login', 'ip': '192.168.1.1', 'browser': 'Chrome'},
],
);
// Query with JSONB operators
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
);
Arrays #
// INSERT with an array
await pool.execute(
r'INSERT INTO produk (nama, tag) VALUES ($1, $2)',
parameters: [
'Laptop Gaming',
['gaming', 'laptop', 'electronics'], // Dart List → PostgreSQL array
],
);
// Query with array operators
final hasilArray = await pool.execute(
r"SELECT * FROM produk WHERE $1 = ANY(tag)",
parameters: ['gaming'], // find products that have the 'gaming' tag
);
// Overlap: arrays that share at least one element
final overlap = await pool.execute(
r'SELECT * FROM produk WHERE tag && $1',
parameters: [['gaming', 'laptop']],
);
UUID #
// PostgreSQL has a native UUID type
await pool.execute(
r'INSERT INTO sesi (id, id_pengguna, dibuat_pada) VALUES (gen_random_uuid(), $1, NOW())',
parameters: [42],
);
// Query with UUID
final sesi = await pool.execute(
r'SELECT * FROM sesi WHERE id = $1::uuid',
parameters: ['550e8400-e29b-41d4-a716-446655440000'],
);
COPY — Very Fast Bulk Inserts #
To insert thousands to millions of rows very quickly, use COPY — far more efficient than one-by-one INSERTs:
import 'package:postgres/postgres.dart';
Future<void> bulkInsert(Connection conn, List<Map<String, dynamic>> data) async {
// COPY is a PostgreSQL-specific command for bulk loading
// Can be 10-100x faster than repeated INSERTs
// Method 1: COPY FROM STDIN with CSV strings
await conn.execute('COPY produk (nama, harga, stok, kategori) FROM STDIN CSV');
// Continue with the data...
// Method 2: executeCopySql (more practical)
final csvData = data.map((row) =>
'${row['nama']},${row['harga']},${row['stok']},${row['kategori']}'
).join('\n');
await conn.runTx((tx) async {
// Use unnest for bulk inserts with parameters
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} rows inserted successfully');
}
LISTEN/NOTIFY — Real-Time Notifications #
PostgreSQL supports simple pub/sub through LISTEN and NOTIFY — useful for cross-process notifications or real-time updates:
import 'package:postgres/postgres.dart';
// Subscriber — listen for notifications
Future<void> dengarkan(Connection conn) async {
// Subscribe to a channel
await conn.execute('LISTEN perubahan_stok');
await conn.execute('LISTEN order_baru');
// Listen for incoming notifications
conn.messages.listen((message) {
if (message is ServerMessage) {
print('Message from the server: $message');
}
});
// or use the dedicated notifications stream
await conn.execute('LISTEN test_channel');
print('Listening for notifications...');
// The connection stays active and waits for notifications
}
// Publisher — send notifications
Future<void> kirNotifikasi(Connection conn, String channel, String payload) async {
await conn.execute(
'SELECT pg_notify(\$1, \$2)',
parameters: [channel, payload],
);
// Or with a direct NOTIFY:
// await conn.execute("NOTIFY $channel, '$payload'");
}
// Real-time usage example
Future<void> contohRealTime() async {
// Connection for the subscriber (separate connection!)
final connSub = await Connection.open(
Endpoint(host: 'localhost', database: 'toko', username: 'app', password: 'pass'),
);
// Connection for the publisher
final connPub = await Connection.open(
Endpoint(host: 'localhost', database: 'toko', username: 'app', password: 'pass'),
);
await connSub.execute('LISTEN stok_update');
// When stock changes in another app / database trigger:
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 or UPDATE #
PostgreSQL supports the very powerful INSERT ... ON CONFLICT:
import 'package:postgres/postgres.dart';
Future<void> contohUpsert(Pool pool) async {
// Upsert — insert if it doesn't exist, update if it does
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 that does nothing if it already exists (ignore duplicates)
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 with a condition — only update if the new value differs
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'],
);
}
PostgreSQL Anti-Patterns in Dart #
Not Using a Pool on the Server #
// ANTI-PATTERN: creating a new connection for every request
Future<Response> handler(Request req) async {
final conn = await Connection.open(Endpoint(/* ... */)); // ✗ slow!
final data = await conn.execute('SELECT ...');
await conn.close();
return Response.ok(data);
}
// CORRECT: use a singleton Pool
final _pool = Pool.withEndpoints([Endpoint(/* ... */)],
settings: PoolSettings(maxConnectionCount: 10));
Future<Response> handler(Request req) async {
final data = await _pool.execute('SELECT ...'); // ✓ fast
return Response.ok(data);
}
SELECT * Without Need #
// ANTI-PATTERN: SELECT * — fetch all columns even when not all are needed
final result = await pool.execute('SELECT * FROM produk'); // ✗ wastes bandwidth and memory
// CORRECT: select only the columns needed
final result = await pool.execute(
'SELECT id, nama, harga FROM produk WHERE aktif = true',
);
N+1 Queries — Same as Other Databases #
// ANTI-PATTERN: querying per item inside a 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],
);
}
}
// CORRECT: one query with a 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'],
);
// group in Dart
}
Summary #
- The
postgresv3 package is the best Dart PostgreSQL driver — supporting typed results, connection pools, transactions, prepared statements, and LISTEN/NOTIFY.- Positional placeholders
$1, $2, $3— different from MySQL (:nama) and MSSQL (@nama). Always use parameterized queries, never string interpolation.RETURNING *after INSERT/UPDATE returns the affected row — very useful for getting all fields (including database-set ones) after an operation.- Native PostgreSQL types — the driver returns
int,double,bool,DateTime,Listdirectly without manual parsing like in MySQL.pool.runTx((tx) async {...})handles COMMIT and ROLLBACK automatically — far safer than manual BEGIN/COMMIT.- JSONB allows storing queryable, indexable JSON in PostgreSQL — ideal for flexible semi-structured data.
- PostgreSQL arrays can be filled directly from Dart
Lists and queried with theANY,ALL,&&(overlap),@>(contains) operators.unnest($1::text[], $2::int[])for type-safe, efficient bulk inserts — better thanexecuteCopyfor most cases.LISTEN/NOTIFYenables pub/sub between database connections — useful for real-time update triggers without polling.ON CONFLICT DO UPDATE(upsert) andON CONFLICT DO NOTHINGreplace MySQL’sINSERT IGNOREandON DUPLICATE KEY UPDATEwith more expressive syntax.