PostgreSQL #
PostgreSQL adalah database relasional open-source paling canggih yang tersedia saat ini. Di balik lisensi gratis, PostgreSQL menawarkan fitur yang bahkan mengalahkan banyak database komersial: tipe data yang sangat kaya (UUID, JSONB, Array, tipe geometri), window functions yang kuat, full-text search bawaan, LISTEN/NOTIFY untuk notifikasi real-time, dan kepatuhan SQL standar yang tinggi. PostgreSQL sering disebut sebagai “database yang paling benar” — ia sangat ketat dengan tipe data dan transaksi. Artikel ini membahas cara koneksi dengan JDBC, memanfaatkan tipe data unik PostgreSQL dari Java, UPSERT dengan ON CONFLICT, LISTEN/NOTIFY untuk event-driven architecture, CopyManager untuk impor data massal, serta Spring Boot JPA dengan fitur-fitur khas PostgreSQL.
Keunggulan dan Karakteristik Unik PostgreSQL #
PostgreSQL bukan sekadar “MySQL yang lebih baik” — ia punya filosofi dan fitur yang berbeda secara fundamental.
| Aspek | MySQL | SQL Server | Oracle | PostgreSQL |
|---|---|---|---|---|
| Auto-increment | AUTO_INCREMENT | IDENTITY | SEQUENCE | SERIAL / BIGSERIAL / GENERATED AS IDENTITY |
| UUID native | ✗ Tidak ada | ✗ UNIQUEIDENTIFIER | ✗ Tidak ada | ✓ UUID tipe bawaan |
| JSON/JSONB | JSON (teks biasa) | JSON (teks biasa) | JSON (teks biasa) | ✓ JSONB (binary, terindeks!) |
| Array | ✗ Tidak ada | ✗ Tidak ada | ✗ Tidak ada | ✓ INTEGER[], TEXT[], dll. |
| Enum tipe | ENUM | ✗ Tidak ada | ✗ Tidak ada | ✓ CREATE TYPE ... AS ENUM |
| Full-text search | Terbatas | Ada | Ada | ✓ Sangat kuat, bawaan |
| UPSERT | INSERT ... ON DUPLICATE KEY | MERGE | MERGE | ✓ INSERT ... ON CONFLICT |
| Notifikasi async | ✗ Tidak ada | Service Broker | Advanced Queuing | ✓ LISTEN/NOTIFY |
| String kosong | '' ≠ NULL | '' ≠ NULL | '' = NULL | '' ≠ NULL |
| Case-sensitive | ✗ Default tidak | ✗ Default tidak | ✗ Default tidak | ✓ Default ya |
| Lisensi | GPL | Komersial | Komersial | PostgreSQL License (bebas) |
flowchart TB
A["Aplikasi Java"] --> B["Spring Data JPA\n(@Entity, Repository)"]
A --> C["JDBC / JdbcTemplate"]
B --> D["Hibernate\n(PostgreSQLDialect)"]
C --> E["HikariCP\n(Connection Pool)"]
D --> E
E --> F["PostgreSQL JDBC Driver\n(pgjdbc)"]
F --> G[("PostgreSQL Server\n(Local / Cloud / RDS / Supabase)")]Persiapan — Driver dan Database #
Dependensi #
<!-- Maven — driver resmi PostgreSQL (pgjdbc) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<!-- HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
// Gradle
implementation 'org.postgresql:postgresql:42.7.3'
implementation 'com.zaxxer:HikariCP:5.1.0'
Menyiapkan Database dan Tabel #
-- Buat database
CREATE DATABASE tokodb
ENCODING 'UTF8'
LC_COLLATE 'id_ID.UTF-8'
LC_CTYPE 'id_ID.UTF-8'
TEMPLATE template0;
\c tokodb
-- Buat tipe ENUM kustom
CREATE TYPE status_produk AS ENUM ('aktif', 'nonaktif', 'habis');
CREATE TYPE kategori_produk AS ENUM ('elektronik', 'aksesori', 'penyimpanan', 'komponen', 'umum');
-- Aktifkan ekstensi untuk UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- alternatif untuk UUID generation
-- Tabel dengan berbagai tipe data PostgreSQL
CREATE TABLE produk (
id BIGSERIAL PRIMARY KEY, -- auto-increment
kode UUID DEFAULT gen_random_uuid() UNIQUE, -- UUID otomatis
nama VARCHAR(255) NOT NULL,
harga NUMERIC(15,2) NOT NULL,
stok INTEGER NOT NULL DEFAULT 0,
kategori kategori_produk, -- ENUM kustom
status status_produk NOT NULL DEFAULT 'aktif', -- ENUM kustom
tag TEXT[], -- Array teks
spesifikasi JSONB, -- JSON binary (terindeks)
aktif BOOLEAN NOT NULL DEFAULT TRUE, -- BOOLEAN native!
dibuat_pada TIMESTAMPTZ DEFAULT NOW(), -- timestamp dengan timezone
diubah_pada TIMESTAMPTZ DEFAULT NOW()
);
-- Index untuk performa
CREATE INDEX idx_produk_kategori ON produk(kategori);
CREATE INDEX idx_produk_status ON produk(status);
CREATE INDEX idx_produk_tag ON produk USING GIN(tag); -- index untuk array
CREATE INDEX idx_produk_spek ON produk USING GIN(spesifikasi); -- index untuk JSONB
-- Trigger untuk update otomatis diubah_pada
CREATE OR REPLACE FUNCTION fn_update_diubah_pada()
RETURNS TRIGGER AS $$
BEGIN
NEW.diubah_pada = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_produk_diubah
BEFORE UPDATE ON produk
FOR EACH ROW
EXECUTE FUNCTION fn_update_diubah_pada();
-- Data awal
INSERT INTO produk (nama, harga, stok, kategori, tag, spesifikasi) VALUES
('Laptop ProBook', 12000000, 5, 'elektronik',
ARRAY['laptop', 'kerja', 'premium'],
'{"ram": "16GB", "storage": "512GB SSD", "layar": "14 inch"}'),
('Mouse Wireless', 150000, 20, 'aksesori',
ARRAY['mouse', 'wireless'],
'{"dpi": 1600, "tombol": 6, "koneksi": "USB-A"}'),
('SSD 1TB', 750000, 30, 'penyimpanan',
ARRAY['ssd', 'nvme'],
'{"kapasitas": "1TB", "antarmuka": "NVMe", "kecepatan_baca": "3500MB/s"}');
COMMIT;
Koneksi JDBC #
Koneksi Dasar #
import java.sql.*;
// URL: jdbc:postgresql://host:port/database
String url = "jdbc:postgresql://localhost:5432/tokodb";
String username = "postgres";
String password = "rahasiaku";
try (Connection conn = DriverManager.getConnection(url, username, password)) {
System.out.println("PostgreSQL: " + conn.getMetaData().getDatabaseProductVersion());
} catch (SQLException e) {
System.err.println("Koneksi gagal: " + e.getMessage());
}
// URL dengan parameter tambahan
String urlParam = "jdbc:postgresql://localhost:5432/tokodb"
+ "?currentSchema=public" // skema default
+ "&ssl=false" // nonaktifkan SSL (development)
+ "&connectTimeout=10" // timeout koneksi (detik)
+ "&socketTimeout=30" // timeout socket (detik)
+ "&ApplicationName=TokoDB-App" // nama aplikasi (tampil di pg_stat_activity)
+ "&stringtype=unspecified"; // kirim String tanpa tipe — lebih fleksibel
SELECT dan Tipe Data PostgreSQL #
try (Connection conn = DriverManager.getConnection(url, username, password)) {
String sql = """
SELECT id, kode, nama, harga, stok, kategori, status,
tag, spesifikasi, aktif, dibuat_pada
FROM produk
WHERE aktif = true AND kategori = ?::kategori_produk
ORDER BY harga DESC
LIMIT ?
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "elektronik"); // Cast ke ENUM dengan ::
ps.setInt(2, 10);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String kode = rs.getString("kode"); // UUID sebagai String
String nama = rs.getString("nama");
java.math.BigDecimal harga = rs.getBigDecimal("harga");
int stok = rs.getInt("stok");
String kat = rs.getString("kategori"); // ENUM sebagai String
boolean aktif = rs.getBoolean("aktif"); // BOOLEAN native!
// Array PostgreSQL
java.sql.Array tagArray = rs.getArray("tag");
String[] tags = (String[]) tagArray.getArray(); // cast ke Java array
// JSONB sebagai String — parse dengan Jackson/Gson
String spesJson = rs.getString("spesifikasi");
// TIMESTAMPTZ sebagai OffsetDateTime
java.time.OffsetDateTime dibuat =
rs.getObject("dibuat_pada", java.time.OffsetDateTime.class);
System.out.printf("[%d] %s - Rp%,.2f (%s) tags: %s%n",
id, nama, harga, kat, java.util.Arrays.toString(tags));
}
}
}
}
INSERT dengan Tipe Data Kaya #
import org.postgresql.util.PGobject;
try (Connection conn = DriverManager.getConnection(url, username, password)) {
String sql = """
INSERT INTO produk (nama, harga, stok, kategori, tag, spesifikasi)
VALUES (?, ?, ?, ?::kategori_produk, ?, ?::jsonb)
RETURNING id, kode
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "Monitor 4K 27\"");
ps.setBigDecimal(2, new java.math.BigDecimal("4500000"));
ps.setInt(3, 3);
ps.setString(4, "elektronik"); // ENUM: cast dengan ::
// Array PostgreSQL — pakai conn.createArrayOf()
java.sql.Array tagArray = conn.createArrayOf("text",
new String[]{"monitor", "4k", "profesional"});
ps.setArray(5, tagArray);
// JSONB — pakai PGobject atau cast string dengan ::jsonb
String spekJson = """
{"resolusi": "3840x2160", "refresh": "144Hz", "panel": "IPS"}
""";
ps.setString(6, spekJson.trim()); // cast ::jsonb di SQL
// RETURNING id, kode — PostgreSQL-specific, ambil sekaligus
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
System.out.println("ID baru: " + rs.getLong("id"));
System.out.println("Kode UUID: " + rs.getString("kode"));
}
}
// Bersihkan array object
tagArray.free();
}
}
Tipe Data Unik PostgreSQL dari Java #
UUID #
import java.util.UUID;
// UUID di PostgreSQL bisa di-set sebagai java.util.UUID atau String
String sqlInsert = "INSERT INTO produk (kode, nama, harga, stok) VALUES (?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement(sqlInsert)) {
UUID kodeBaru = UUID.randomUUID();
ps.setObject(1, kodeBaru); // setObject() untuk UUID
ps.setString(2, "Headset Gaming");
ps.setBigDecimal(3, new java.math.BigDecimal("650000"));
ps.setInt(4, 8);
ps.executeUpdate();
}
// Baca UUID
String sqlSelect = "SELECT kode FROM produk WHERE id = ?";
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement(sqlSelect)) {
ps.setLong(1, 1L);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
UUID kode = rs.getObject("kode", UUID.class); // Java 8+ cara bersih
// atau: UUID.fromString(rs.getString("kode"))
System.out.println("UUID: " + kode);
}
}
}
JSONB — Menyimpan dan Mencari JSON #
import com.fasterxml.jackson.databind.ObjectMapper;
import org.postgresql.util.PGobject;
ObjectMapper objectMapper = new ObjectMapper();
// Menyimpan JSONB — dua cara
// Cara 1: cast di SQL dengan ::jsonb (lebih sederhana)
String sql1 = "UPDATE produk SET spesifikasi = ?::jsonb WHERE id = ?";
// Cara 2: PGobject (lebih eksplisit)
String sql2 = "UPDATE produk SET spesifikasi = ? WHERE id = ?";
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement(sql2)) {
// Buat objek Java, konversi ke JSON string
var spek = java.util.Map.of(
"ram", "32GB",
"storage", "1TB NVMe",
"layar", "15.6 inch",
"gpu", "RTX 4060"
);
String jsonStr = objectMapper.writeValueAsString(spek);
// Bungkus dengan PGobject bertipe jsonb
PGobject jsonb = new PGobject();
jsonb.setType("jsonb");
jsonb.setValue(jsonStr);
ps.setObject(1, jsonb);
ps.setLong(2, 1L);
ps.executeUpdate();
}
// Mencari berdasarkan JSONB — operator PostgreSQL
try (Connection conn = DriverManager.getConnection(url, username, password)) {
// @> : JSONB containment — cari produk yang spesifikasi mengandung key tertentu
String sqlContain = """
SELECT id, nama, spesifikasi
FROM produk
WHERE spesifikasi @> ?::jsonb
""";
try (PreparedStatement ps = conn.prepareStatement(sqlContain)) {
// Cari produk yang RAM-nya 16GB
ps.setString(1, "{\"ram\": \"16GB\"}");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("nama") + ": "
+ rs.getString("spesifikasi"));
}
}
}
// ->> : ambil nilai dari JSONB sebagai teks
String sqlExtract = """
SELECT nama, spesifikasi->>'ram' AS ram, spesifikasi->>'storage' AS storage
FROM produk
WHERE spesifikasi IS NOT NULL
AND (spesifikasi->>'ram') IS NOT NULL
""";
try (PreparedStatement ps = conn.prepareStatement(sqlExtract);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.printf("%-20s RAM: %-6s Storage: %s%n",
rs.getString("nama"),
rs.getString("ram"),
rs.getString("storage"));
}
}
}
Array PostgreSQL #
try (Connection conn = DriverManager.getConnection(url, username, password)) {
// Cari produk yang punya tag tertentu — operator @> (array containment)
String sqlTag = """
SELECT id, nama, tag
FROM produk
WHERE tag @> ?::text[] -- produk yang tagnya mengandung semua tag ini
AND aktif = true
""";
try (PreparedStatement ps = conn.prepareStatement(sqlTag)) {
java.sql.Array tagCari = conn.createArrayOf("text", new String[]{"laptop"});
ps.setArray(1, tagCari);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
java.sql.Array tags = rs.getArray("tag");
String[] tagArr = (String[]) tags.getArray();
System.out.println(rs.getString("nama") + ": "
+ java.util.Arrays.toString(tagArr));
tags.free();
}
}
tagCari.free();
}
// Cari produk yang punya ANY dari tag ini — operator &&
String sqlAnyTag = """
SELECT id, nama FROM produk
WHERE tag && ?::text[] -- ada irisan antara tag produk dan tag yang dicari
""";
try (PreparedStatement ps = conn.prepareStatement(sqlAnyTag)) {
java.sql.Array tagCari = conn.createArrayOf("text",
new String[]{"laptop", "monitor", "gaming"});
ps.setArray(1, tagCari);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getString("nama"));
}
tagCari.free();
}
// Tambah elemen ke array — operator array_append
String sqlTambahTag = """
UPDATE produk
SET tag = array_append(tag, ?)
WHERE id = ? AND NOT (tag @> ARRAY[?]::text[])
""";
try (PreparedStatement ps = conn.prepareStatement(sqlTambahTag)) {
ps.setString(1, "diskon");
ps.setLong(2, 1L);
ps.setString(3, "diskon"); // jangan tambah jika sudah ada
ps.executeUpdate();
}
}
UPSERT — INSERT atau UPDATE #
PostgreSQL mendukung INSERT ... ON CONFLICT (UPSERT) yang jauh lebih elegandaripada cek manual atau subquery yang berbelit.
ON CONFLICT DO UPDATE #
// Skenario: import data produk — update jika kode sudah ada, insert jika belum
String sqlUpsert = """
INSERT INTO produk (kode, nama, harga, stok, kategori)
VALUES (?::uuid, ?, ?, ?, ?::kategori_produk)
ON CONFLICT (kode) DO UPDATE SET
nama = EXCLUDED.nama,
harga = EXCLUDED.harga,
stok = EXCLUDED.stok,
kategori = EXCLUDED.kategori,
diubah_pada = NOW()
RETURNING id, (xmax = 0) AS is_insert
""";
// EXCLUDED merujuk ke nilai yang AKAN diinsert
// xmax = 0 berarti baris baru (insert), xmax != 0 berarti baris lama (update)
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement(sqlUpsert)) {
ps.setString(1, "123e4567-e89b-12d3-a456-426614174000");
ps.setString(2, "Laptop Gaming");
ps.setBigDecimal(3, new java.math.BigDecimal("15000000"));
ps.setInt(4, 3);
ps.setString(5, "elektronik");
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
boolean isInsert = rs.getBoolean("is_insert");
System.out.println("Operasi: " + (isInsert ? "INSERT baru" : "UPDATE existing"));
System.out.println("ID: " + rs.getLong("id"));
}
}
}
// ON CONFLICT DO NOTHING — abaikan jika duplikat (tidak update, tidak error)
String sqlIgnore = """
INSERT INTO produk (kode, nama, harga, stok)
VALUES (?::uuid, ?, ?, ?)
ON CONFLICT (kode) DO NOTHING
""";
UPSERT untuk Increment Stok #
// Skenario: tambah stok — buat baris baru jika produk belum ada,
// atau tambahkan stok jika sudah ada
String sqlTambahStok = """
INSERT INTO produk (kode, nama, harga, stok, kategori)
VALUES (?::uuid, ?, ?, ?, ?::kategori_produk)
ON CONFLICT (kode) DO UPDATE SET
stok = produk.stok + EXCLUDED.stok -- tambahkan, bukan ganti
RETURNING id, stok AS stok_terbaru
""";
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement(sqlTambahStok)) {
ps.setString(1, "123e4567-e89b-12d3-a456-426614174000");
ps.setString(2, "Laptop Gaming");
ps.setBigDecimal(3, new java.math.BigDecimal("15000000"));
ps.setInt(4, 5); // tambah 5 unit
ps.setString(5, "elektronik");
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
System.out.println("Stok terbaru: " + rs.getInt("stok_terbaru"));
}
}
}
LISTEN/NOTIFY — Notifikasi Real-Time #
PostgreSQL punya fitur unik: LISTEN/NOTIFY yang memungkinkan satu koneksi mengirim notifikasi ke koneksi lain yang mendengarkan channel tertentu. Ini bisa dipakai untuk event-driven architecture tanpa message broker eksternal.
Server (Penerima LISTEN) #
import org.postgresql.PGConnection;
import org.postgresql.PGNotification;
// Koneksi khusus untuk LISTEN — jangan pakai dari connection pool
String url = "jdbc:postgresql://localhost:5432/tokodb";
Connection listenConn = DriverManager.getConnection(url, "postgres", "rahasiaku");
try {
// Mulai mendengarkan channel "produk_berubah"
try (Statement stmt = listenConn.createStatement()) {
stmt.execute("LISTEN produk_berubah");
stmt.execute("LISTEN stok_habis");
}
System.out.println("Mendengarkan notifikasi...");
// Loop polling notifikasi (tidak blocking)
while (!Thread.currentThread().isInterrupted()) {
// Kirim query kosong untuk flush notifikasi dari server
try (Statement stmt = listenConn.createStatement()) {
stmt.execute("SELECT 1");
}
// Ambil semua notifikasi yang masuk
PGConnection pgConn = listenConn.unwrap(PGConnection.class);
PGNotification[] notifs = pgConn.getNotifications(1000); // timeout 1 detik
if (notifs != null) {
for (PGNotification notif : notifs) {
System.out.printf("[Notifikasi] Channel: %s | PID: %d | Payload: %s%n",
notif.getName(),
notif.getPID(),
notif.getParameter());
// Proses notifikasi berdasarkan channel
switch (notif.getName()) {
case "produk_berubah" -> prosesPerubahanProduk(notif.getParameter());
case "stok_habis" -> prosesStokHabis(notif.getParameter());
}
}
}
}
} finally {
listenConn.close();
}
Klien (Pengirim NOTIFY) #
// Kirim notifikasi dari Java
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement("SELECT pg_notify(?, ?)")) {
// Format payload sebagai JSON
String payload = "{\"id\": 1, \"aksi\": \"update\", \"field\": \"harga\"}";
ps.setString(1, "produk_berubah");
ps.setString(2, payload);
ps.execute();
System.out.println("Notifikasi terkirim");
}
// Atau langsung dari SQL (misalnya dalam trigger)
// Di SQL: PERFORM pg_notify('stok_habis', row_to_json(NEW)::text);
Trigger NOTIFY Otomatis #
-- Trigger: kirim notifikasi otomatis saat stok di bawah ambang
CREATE OR REPLACE FUNCTION fn_cek_stok()
RETURNS TRIGGER AS $$
BEGIN
-- Notifikasi saat stok update
PERFORM pg_notify('produk_berubah',
json_build_object(
'id', NEW.id,
'nama', NEW.nama,
'stok_lama', OLD.stok,
'stok_baru', NEW.stok
)::text
);
-- Notifikasi khusus jika stok habis
IF NEW.stok = 0 AND OLD.stok > 0 THEN
PERFORM pg_notify('stok_habis',
json_build_object('id', NEW.id, 'nama', NEW.nama)::text
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_cek_stok
AFTER UPDATE OF stok ON produk
FOR EACH ROW
EXECUTE FUNCTION fn_cek_stok();
CopyManager — Bulk Import Sangat Cepat #
CopyManager PostgreSQL memungkinkan impor data massal dari CSV atau stream — jauh lebih cepat dari batch INSERT biasa.
Import dari CSV #
import org.postgresql.copy.CopyManager;
import org.postgresql.core.BaseConnection;
// Data CSV (bisa dari file, string, atau stream)
String csvData = """
Laptop A,8000000,5,elektronik
Laptop B,9000000,3,elektronik
Mouse Ergonomis,250000,15,aksesori
SSD 512GB,500000,20,penyimpanan
""";
try (Connection conn = DriverManager.getConnection(url, username, password)) {
CopyManager copyManager = new CopyManager((BaseConnection) conn);
// COPY FROM STDIN — import dari Java ke PostgreSQL
String copySql = """
COPY produk (nama, harga, stok, kategori)
FROM STDIN
WITH (FORMAT csv, DELIMITER ',', NULL '\\N')
""";
long jumlahBaris = copyManager.copyIn(
copySql,
new java.io.StringReader(csvData)
);
System.out.println("Berhasil import: " + jumlahBaris + " baris");
}
// Import dari file CSV besar (stream-based, hemat memori)
try (Connection conn = DriverManager.getConnection(url, username, password);
java.io.InputStream is = new java.io.FileInputStream("data/produk.csv")) {
CopyManager copyManager = new CopyManager((BaseConnection) conn);
String copySql = "COPY produk (nama, harga, stok, kategori) FROM STDIN WITH (FORMAT csv, HEADER true)";
long baris = copyManager.copyIn(copySql, is);
System.out.println("Diimpor: " + baris + " baris");
}
Export ke CSV #
// COPY TO STDOUT — export dari PostgreSQL ke Java
try (Connection conn = DriverManager.getConnection(url, username, password)) {
CopyManager copyManager = new CopyManager((BaseConnection) conn);
String copySql = """
COPY (
SELECT id, nama, harga, stok, kategori
FROM produk
WHERE aktif = true
ORDER BY id
) TO STDOUT WITH (FORMAT csv, HEADER true, DELIMITER ',')
""";
java.io.StringWriter writer = new java.io.StringWriter();
long baris = copyManager.copyOut(copySql, writer);
System.out.println("Diekspor: " + baris + " baris");
System.out.println(writer.toString());
// Export langsung ke file
try (java.io.OutputStream os = new java.io.FileOutputStream("export/produk.csv")) {
copyManager.copyOut(copySql, os);
}
}
HikariCP untuk PostgreSQL #
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class PostgresPool {
private static final HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/tokodb");
config.setUsername("postgres");
config.setPassword("rahasiaku");
config.setDriverClassName("org.postgresql.Driver");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000);
config.setIdleTimeout(600_000);
config.setMaxLifetime(1_800_000);
// PostgreSQL: tidak perlu connectionTestQuery
// HikariCP pakai isValid() yang lebih efisien
// config.setConnectionTestQuery("SELECT 1"); // opsional
// Property khusus PostgreSQL
config.addDataSourceProperty("ApplicationName", "TokoDB-App");
config.addDataSourceProperty("stringtype", "unspecified"); // kirim String tanpa tipe
config.addDataSourceProperty("reWriteBatchedInserts", "true"); // optimasi batch INSERT
config.addDataSourceProperty("prepareThreshold", "5"); // server-side prepared statement
config.setPoolName("Postgres-Pool");
dataSource = new HikariDataSource(config);
}
public static java.sql.Connection getConnection() throws java.sql.SQLException {
return dataSource.getConnection();
}
}
Spring Boot + Spring Data JPA #
Dependensi #
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Konfigurasi application.yml #
spring:
datasource:
url: jdbc:postgresql://localhost:5432/tokodb
username: postgres
password: rahasiaku
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
pool-name: Postgres-Pool
data-source-properties:
ApplicationName: TokoDB-Spring
stringtype: unspecified
reWriteBatchedInserts: true
jpa:
hibernate:
ddl-auto: validate
show-sql: false
open-in-view: false
database-platform: org.hibernate.dialect.PostgreSQLDialect
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
jdbc:
batch_size: 50
fetch_size: 100
default_schema: public
Entity dengan Tipe Data PostgreSQL #
import jakarta.persistence.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.UUID;
@Entity
@Table(name = "produk")
public class Produk {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// UUID — kolom terpisah, bukan PK
@Column(unique = true, updatable = false,
columnDefinition = "UUID DEFAULT gen_random_uuid()")
private UUID kode;
@Column(nullable = false, length = 255)
private String nama;
@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal harga;
@Column(nullable = false)
private Integer stok = 0;
// ENUM PostgreSQL — perlu @Enumerated dan columnDefinition
@Enumerated(EnumType.STRING)
@Column(columnDefinition = "kategori_produk")
private KategoriProduk kategori;
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "status_produk DEFAULT 'aktif'")
private StatusProduk status = StatusProduk.AKTIF;
// Array PostgreSQL
@Column(columnDefinition = "TEXT[]")
private String[] tag;
// JSONB PostgreSQL — Hibernate 6+ mendukung Map ke JSONB
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "JSONB")
private Map<String, Object> spesifikasi;
// BOOLEAN native PostgreSQL
@Column(nullable = false)
private Boolean aktif = true;
@Column(name = "dibuat_pada", updatable = false,
columnDefinition = "TIMESTAMPTZ DEFAULT NOW()")
private OffsetDateTime dibuatPada;
@Column(name = "diubah_pada",
columnDefinition = "TIMESTAMPTZ DEFAULT NOW()")
private OffsetDateTime diubahPada;
@PrePersist
protected void onCreate() {
dibuatPada = OffsetDateTime.now();
diubahPada = OffsetDateTime.now();
if (kode == null) kode = UUID.randomUUID();
}
@PreUpdate
protected void onUpdate() {
diubahPada = OffsetDateTime.now();
}
// Enum types
public enum KategoriProduk { elektronik, aksesori, penyimpanan, komponen, umum }
public enum StatusProduk { aktif, nonaktif, habis }
// Konstruktor, getter, setter
public Produk() {}
public Produk(String nama, BigDecimal harga, int stok, KategoriProduk kategori) {
this.nama = nama; this.harga = harga; this.stok = stok; this.kategori = kategori;
}
// ... getter dan setter
}
Repository dengan Fitur PostgreSQL #
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@Repository
public interface ProdukRepository extends JpaRepository<Produk, Long> {
// Method name — sama seperti database lain
List<Produk> findByAktifTrue();
List<Produk> findByKategoriAndAktifTrue(Produk.KategoriProduk kategori);
Optional<Produk> findByKode(UUID kode);
// Native query PostgreSQL — JSONB, array, ILIKE
@Query(value = """
SELECT id, nama, harga
FROM produk
WHERE aktif = true
AND nama ILIKE CONCAT('%', :kata, '%') -- ILIKE: case-insensitive LIKE
ORDER BY harga DESC
LIMIT :limit
""", nativeQuery = true)
List<Object[]> cariCaseInsensitive(@Param("kata") String kata, @Param("limit") int limit);
// Cari berdasarkan JSONB
@Query(value = """
SELECT * FROM produk
WHERE spesifikasi @> CAST(:json AS jsonb)
AND aktif = true
""", nativeQuery = true)
List<Produk> findBySpesifikasi(@Param("json") String jsonFragment);
// Cari berdasarkan tag (array containment)
@Query(value = """
SELECT * FROM produk
WHERE tag @> CAST(:tags AS text[])
AND aktif = true
""", nativeQuery = true)
List<Produk> findByTags(@Param("tags") String tagsArray); // "{laptop,gaming}"
// UPSERT via native query
@Modifying
@Query(value = """
INSERT INTO produk (kode, nama, harga, stok, kategori)
VALUES (CAST(:kode AS uuid), :nama, :harga, :stok, CAST(:kategori AS kategori_produk))
ON CONFLICT (kode) DO UPDATE SET
nama = EXCLUDED.nama, harga = EXCLUDED.harga,
stok = EXCLUDED.stok, diubah_pada = NOW()
""", nativeQuery = true)
void upsert(@Param("kode") String kode, @Param("nama") String nama,
@Param("harga") BigDecimal harga, @Param("stok") int stok,
@Param("kategori") String kategori);
// Full-text search PostgreSQL
@Query(value = """
SELECT * FROM produk
WHERE to_tsvector('indonesian', nama || ' ' || COALESCE(kategori::text, ''))
@@ plainto_tsquery('indonesian', :query)
AND aktif = true
""", nativeQuery = true)
List<Produk> fullTextSearch(@Param("query") String query);
// Window function — ranking per kategori
@Query(value = """
SELECT id, nama, harga, kategori,
RANK() OVER (PARTITION BY kategori ORDER BY harga DESC) AS peringkat
FROM produk
WHERE aktif = true
""", nativeQuery = true)
List<Object[]> rankingPerKategori();
}
Tips dan Fitur Lanjutan #
Transaksi dengan Advisory Lock #
PostgreSQL punya advisory lock — aplikasi-level lock yang tidak terikat ke tabel atau baris tertentu, berguna untuk mencegah proses duplikat dalam sistem terdistribusi.
try (Connection conn = DriverManager.getConnection(url, username, password)) {
conn.setAutoCommit(false);
// Minta advisory lock dengan key numerik
long lockKey = 12345L;
try (PreparedStatement ps = conn.prepareStatement("SELECT pg_advisory_xact_lock(?)")) {
ps.setLong(1, lockKey);
ps.execute();
// Lock otomatis dilepas saat transaksi selesai (commit/rollback)
}
// Proses yang harus berjalan eksklusif
System.out.println("Memproses dalam advisory lock...");
// ...
conn.commit();
}
EXPLAIN ANALYZE dari Java #
try (Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement ps = conn.prepareStatement(
"EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM produk WHERE aktif = true")) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString(1)); // JSON execution plan
}
}
}
Kapan Menggunakan PostgreSQL #
Gunakan POSTGRESQL jika:
✓ Butuh database open-source tanpa biaya lisensi dengan fitur enterprise
✓ Data model kompleks: JSONB untuk data semi-terstruktur, Array untuk list
✓ Perlu UPSERT yang bersih dan ekspresif (ON CONFLICT)
✓ Butuh notifikasi real-time tanpa broker eksternal (LISTEN/NOTIFY)
✓ Full-text search bawaan tanpa Elasticsearch untuk kebutuhan sederhana
✓ Tim yang menghargai kepatuhan SQL standar dan ACID yang ketat
✓ Deployment di cloud: AWS RDS, Google Cloud SQL, Azure, Supabase, Neon
Hal yang perlu diperhatikan:
✗ Case-sensitive secara default — 'Laptop' ≠ 'laptop' saat query persis
✗ ENUM PostgreSQL sulit diubah (ALTER TYPE butuh workaround) — pertimbangkan teks biasa
✗ VACUUM otomatis untuk dead tuple — pantau pengaturan autovacuum di tabel besar
✗ Tidak ada "LIMIT" pada UPDATE/DELETE tanpa subquery
✗ Advisory lock key harus dikelola sendiri agar tidak tabrakan antar fitur
Ringkasan #
- Driver
org.postgresql:postgresqladalah satu-satunya driver resmi. Tidak seperti Oracle yang punya banyak versi JAR, satu versi pgjdbc bekerja dengan semua versi PostgreSQL.BOOLEANnative,UUIDnative,JSONBterindeks — tiga keunggulan tipe data PostgreSQL yang langsung tersedia tanpa workaround. Gunakanrs.getBoolean(),rs.getObject("col", UUID.class), danPGobjectuntuk JSONB.conn.createArrayOf("text", array)untuk membuat array PostgreSQL dari Java. Baca kembali denganrs.getArray()lalu cast keString[].RETURNINGsetelah INSERT/UPDATE/DELETE — PostgreSQL bisa mengembalikan baris yang terpengaruh langsung tanpa query terpisah. Gunakan denganps.executeQuery()(bukanexecuteUpdate()).ON CONFLICT DO UPDATEadalah cara UPSERT yang bersih —EXCLUDED.kolommerujuk ke nilai yang ingin diinsert, sehingga bisa dipakai untuk update atau increment.LISTEN/NOTIFYuntuk notifikasi real-time antar koneksi database — alternatif ringan untuk message broker dalam satu infrastruktur PostgreSQL.CopyManageruntuk bulk import — jauh lebih cepat dari batch INSERT biasa. Impor jutaan baris dari CSV dalam hitungan detik.reWriteBatchedInserts=truedi HikariCP — property PostgreSQL yang mengubahINSERT ... VALUES (?) (?) (?)menjadi satu statement multi-row, meningkatkan performa batch secara signifikan.ILIKEuntuk pencarian case-insensitive (bukanLIKE). PostgreSQL membedakan keduanya karena secara default case-sensitive.