PostgreSQL

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.

AspekMySQLSQL ServerOraclePostgreSQL
Auto-incrementAUTO_INCREMENTIDENTITYSEQUENCESERIAL / BIGSERIAL / GENERATED AS IDENTITY
UUID native✗ Tidak adaUNIQUEIDENTIFIER✗ Tidak adaUUID tipe bawaan
JSON/JSONBJSON (teks biasa)JSON (teks biasa)JSON (teks biasa)JSONB (binary, terindeks!)
Array✗ Tidak ada✗ Tidak ada✗ Tidak adaINTEGER[], TEXT[], dll.
Enum tipeENUM✗ Tidak ada✗ Tidak adaCREATE TYPE ... AS ENUM
Full-text searchTerbatasAdaAda✓ Sangat kuat, bawaan
UPSERTINSERT ... ON DUPLICATE KEYMERGEMERGEINSERT ... ON CONFLICT
Notifikasi async✗ Tidak adaService BrokerAdvanced QueuingLISTEN/NOTIFY
String kosong''NULL''NULL'' = NULL''NULL
Case-sensitive✗ Default tidak✗ Default tidak✗ Default tidak✓ Default ya
LisensiGPLKomersialKomersialPostgreSQL 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:postgresql adalah satu-satunya driver resmi. Tidak seperti Oracle yang punya banyak versi JAR, satu versi pgjdbc bekerja dengan semua versi PostgreSQL.
  • BOOLEAN native, UUID native, JSONB terindeks — tiga keunggulan tipe data PostgreSQL yang langsung tersedia tanpa workaround. Gunakan rs.getBoolean(), rs.getObject("col", UUID.class), dan PGobject untuk JSONB.
  • conn.createArrayOf("text", array) untuk membuat array PostgreSQL dari Java. Baca kembali dengan rs.getArray() lalu cast ke String[].
  • RETURNING setelah INSERT/UPDATE/DELETE — PostgreSQL bisa mengembalikan baris yang terpengaruh langsung tanpa query terpisah. Gunakan dengan ps.executeQuery() (bukan executeUpdate()).
  • ON CONFLICT DO UPDATE adalah cara UPSERT yang bersih — EXCLUDED.kolom merujuk ke nilai yang ingin diinsert, sehingga bisa dipakai untuk update atau increment.
  • LISTEN/NOTIFY untuk notifikasi real-time antar koneksi database — alternatif ringan untuk message broker dalam satu infrastruktur PostgreSQL.
  • CopyManager untuk bulk import — jauh lebih cepat dari batch INSERT biasa. Impor jutaan baris dari CSV dalam hitungan detik.
  • reWriteBatchedInserts=true di HikariCP — property PostgreSQL yang mengubah INSERT ... VALUES (?) (?) (?) menjadi satu statement multi-row, meningkatkan performa batch secara signifikan.
  • ILIKE untuk pencarian case-insensitive (bukan LIKE). PostgreSQL membedakan keduanya karena secara default case-sensitive.

← Sebelumnya: Oracle   Berikutnya: MongoDB →

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