Sejauh ini program Java yang kita tulis menyimpan data di memori: begitu proses berakhir, seluruh isi variabel ikut tertutup bersama JVM. Aplikasi nyata — mesin kasir, toko daring, sistem perpustakaan — justru hidup dari data yang bertahan berbulan-bulan dan diubah oleh banyak pengguna hampir pada saat bersamaan. Kebutuhan itulah yang dijawab oleh relational database: data disusun sebagai tabel yang dilengkapi kunci sehingga mudah diperiksa, digabungkan, dan diquery. Chapter ini meletakkan fondasi konsep tabel, kunci, dan relasi supaya pemakaian JDBC pada chapter berikutnya tidak menjadi sekadar hafalan method.
Tabel, Baris, dan Kolom #
Relational database menyimpan data dalam tabel yang merepresentasikan satu jenis entitas: pelanggan, pesanan, produk. Setiap tabel terdiri atas kolom yang mendefinisikan atribut beserta tipenya, dan baris yang menyimpan satu data nyata. Satu baris disebut juga record; satu kolom pada satu baris disebut field atau nilai sel. Tipe paling sering dipakai dirangkum pada tabel berikut:
| Tipe SQL | Makna | Contoh nilai |
|---|---|---|
INTEGER | bilangan bulat | 42 |
VARCHAR(n) | teks dengan panjang maksimum n | 'Budi' |
DECIMAL(p,s) | angka presisi tetap untuk uang | 150000.00 |
DATE / TIMESTAMP | tanggal / tanggal dan waktu | '2026-01-15' |
BOOLEAN | nilai benar atau salah | TRUE |
BLOB | data biner besar (foto, dokumen) | — |
Berbeda dengan List<Pelanggan> di Java, definisi kolom bersifat skema tetap: setiap baris pada tabel yang sama wajib mengikuti kolom dan tipe yang sama. Database engine yang memeriksa batasan tersebut, bukan kode aplikasi kita.
Kunci Primer dan Kunci Tamu #
Primary key (PK) adalah kolom (atau gabungan kolom) yang menjadi identitas unik setiap baris — nilainya wajib terisi dan tidak boleh kembar. Foreign key (FK) adalah kolom yang merujuk primary key di tabel lain, misalnya pesanan.pelanggan_id merujuk pelanggan.id. Mekanisme di balik layarnya sederhana namun kuat: sebelum sebuah INSERT atau UPDATE diterapkan, database engine memeriksa batasan kunci dan menolak operasi yang melanggar, sehingga data yang tidak konsisten tidak pernah tersimpan.
Karena tidak ada server database aktif pada lingkungan uji Course ini, program berikut meniru pemeriksaan kunci tersebut memakai Java murni — logika penolakannya sama dengan yang dilakukan database engine di dalam:
import java.util.ArrayList;
import java.util.List;
public class TabelRelasi {
record Pelanggan(int id, String nama, String kota) {}
record Pesanan(int id, int pelangganId, String barang, long total) {}
record Tag(int id, String label) {}
record PelangganTag(int pelangganId, int tagId) {}
public static void main(String[] args) {
List<Pelanggan> pelanggan = new ArrayList<>(List.of(
new Pelanggan(1, "Budi", "Jakarta"),
new Pelanggan(2, "Siti", "Bandung"),
new Pelanggan(3, "Dewi", "Surabaya")));
List<Pesanan> pesanan = new ArrayList<>(List.of(
new Pesanan(101, 1, "Buku Java", 150_000),
new Pesanan(102, 2, "Mouse", 250_000),
new Pesanan(103, 2, "Keyboard", 450_000)));
System.out.println("== Tabel pelanggan ==");
cetakPelanggan(pelanggan);
// Uji 1: primary key duplikat (id 2 sudah dipakai Siti)
try {
tambahPelanggan(pelanggan, new Pelanggan(2, "Ranto", "Medan"));
} catch (IllegalStateException e) {
System.out.println("DITOLAK: " + e.getMessage());
}
// Uji 2: foreign key tidak valid (pelangganId 99 tidak ada)
try {
tambahPesanan(pelanggan, pesanan, new Pesanan(104, 99, "Monitor", 1_200_000));
} catch (IllegalStateException e) {
System.out.println("DITOLAK: " + e.getMessage());
}
// Uji 3: baris yang sah lolos pemeriksaan kunci
tambahPesanan(pelanggan, pesanan, new Pesanan(104, 3, "Monitor", 1_200_000));
System.out.println("Pesanan 104 tersimpan untuk pelanggan 3 (Dewi).");
// Relasi 1-N: gabungkan pesanan dengan pelanggan pemiliknya
System.out.println();
System.out.println("== Laporan pesanan (gabungan pelanggan <-> pesanan) ==");
for (Pesanan p : pesanan) {
Pelanggan c = cariPelanggan(pelanggan, p.pelangganId());
System.out.printf("%-4d %-6s %-9s %-8s Rp%d%n",
p.id(), c.nama(), p.barang(), c.kota(), p.total());
}
// Relasi N-M: satu pelanggan boleh punya banyak tag, satu tag dipakai
// banyak pelanggan, lewat tabel junction pelanggan_tag
List<Tag> tag = List.of(new Tag(1, "vip"), new Tag(2, "prioritas"));
List<PelangganTag> pelangganTag = List.of(
new PelangganTag(1, 1),
new PelangganTag(1, 2),
new PelangganTag(2, 2),
new PelangganTag(3, 1));
System.out.println();
System.out.println("== Tag pelanggan (relasi N-M lewat tabel junction) ==");
for (Pelanggan c : pelanggan) {
List<String> label = new ArrayList<>();
for (PelangganTag pt : pelangganTag) {
if (pt.pelangganId() == c.id()) {
label.add(cariTag(tag, pt.tagId()).label());
}
}
System.out.printf("%-6s -> %s%n", c.nama(), label);
}
}
static void tambahPelanggan(List<Pelanggan> tabel, Pelanggan baru) {
for (Pelanggan c : tabel) {
if (c.id() == baru.id()) {
throw new IllegalStateException(
"primary key pelanggan.id duplikat: " + baru.id());
}
}
tabel.add(baru);
}
static void tambahPesanan(List<Pelanggan> pelanggan, List<Pesanan> tabel, Pesanan baru) {
if (cariPelanggan(pelanggan, baru.pelangganId()) == null) {
throw new IllegalStateException(
"foreign key pesanan.pelangganId tidak ditemukan: " + baru.pelangganId());
}
tabel.add(baru);
}
static Pelanggan cariPelanggan(List<Pelanggan> tabel, int id) {
for (Pelanggan c : tabel) {
if (c.id() == id) {
return c;
}
}
return null;
}
static Tag cariTag(List<Tag> tabel, int id) {
for (Tag t : tabel) {
if (t.id() == id) {
return t;
}
}
return null;
}
static void cetakPelanggan(List<Pelanggan> tabel) {
System.out.println("| id | nama | kota |");
System.out.println("|----|--------|-----------|");
for (Pelanggan c : tabel) {
System.out.printf("| %-2d | %-6s | %-9s |%n", c.id(), c.nama(), c.kota());
}
}
}Output yang Dihasilkan:
== Tabel pelanggan ==
| id | nama | kota |
|----|--------|-----------|
| 1 | Budi | Jakarta |
| 2 | Siti | Bandung |
| 3 | Dewi | Surabaya |
DITOLAK: primary key pelanggan.id duplikat: 2
DITOLAK: foreign key pesanan.pelangganId tidak ditemukan: 99
Pesanan 104 tersimpan untuk pelanggan 3 (Dewi).
== Laporan pesanan (gabungan pelanggan <-> pesanan) ==
101 Budi Buku Java Jakarta Rp150000
102 Siti Mouse Bandung Rp250000
103 Siti Keyboard Bandung Rp450000
104 Dewi Monitor Surabaya Rp1200000
== Tag pelanggan (relasi N-M lewat tabel junction) ==
Budi -> [vip, prioritas]
Siti -> [prioritas]
Dewi -> [vip]Mari kita telaah bagian inti program di atas:
- Baris 6-9 (
record Pelanggan ...dst.): empatrecordmemodelkan baris tabel —Pelanggan,Pesanan,Tag, dan tabel junctionPelangganTaguntuk relasi banyak-ke-banyak. - Baris 25-30 (
try { tambahPelanggan ... } catch): mencoba menambah pelanggan denganid2 yang sudah dipakai Siti. MethodtambahPelangganmemeriksa seluruh baris mencariidkembar, lalu melemparIllegalStateException— inilah penolakan yang di dunia nyata dilakukan database engine terhadap pelanggaran primary key. - Baris 32-37 (
tambahPesanan ... 99): memasukkan pesanan milikpelangganId99. MethodtambahPesananmemanggilcariPelanggan; karena hasilnyanull, foreign key dinilai tidak valid dan baris ditolak. - Baris 46-50 (perulangan laporan): menggabungkan setiap
PesanandenganPelangganpemiliknya melaluicariPelanggan— bentuk manual dari klausaJOINpada SQL, yang akan kita lihat sebentar lagi. - Baris akhir main (blok tag pelanggan): relasi N-M tidak bisa hidup di satu kolom, sehingga dibutuhkan tabel junction
pelanggan_tagyang barisnya memasangkanpelangganIddengantagId.
Relasi Antar Tabel #
Tiga bentuk relasi menutup hampir semua kebutuhan pemodelan data. Pemilihan bentuknya menentukan letak foreign key:
| Relasi | Contoh | Penempatan kunci |
|---|---|---|
| 1-1 | satu pengguna punya satu profil | FK unik di salah satu sisi |
| 1-N | satu pelanggan punya banyak pesanan | FK di sisi “banyak” (pesanan.pelanggan_id) |
| N-M | pelanggan dan tag saling berhubungan banyak | tabel junction berdua FK |
Bentuk 1-1 sering kali digabung menjadi satu tabel kecuali ada alasan pemisahan (kolom sensitif atau jarang dipakai). Bentuk N-M selalu memakai tabel junction, seperti pelanggan_tag pada contoh program di atas.
Normalisasi Ringkas #
Normalisasi adalah proses menyusun tabel supaya setiap fakta hanya tersimpan di satu tempat. Aturan praktisnya bertingkat: 1NF — setiap sel berisi satu nilai atomik, bukan daftar dipisah koma; 2NF — kolom non-kunci bergantung penuh pada seluruh primary key; 3NF — kolom non-kunci tidak bergantung pada kolom non-kunci lain. Pelanggaran yang paling sering muncul: menyimpan nama_pelanggan di tabel pesanan padahal nama sudah ada di tabel pelanggan — saat nama berubah, kita harus memperbarui banyak baris dan berisiko ada yang terlewat.
SQL DDL dan DML Singkat #
DDL (Data Definition Language) membentuk struktur: CREATE TABLE, ALTER TABLE, DROP TABLE. DML (Data Manipulation Language) mengolah isi data: INSERT, SELECT, UPDATE, DELETE. Skema toko kita di SQL tampak seperti berikut:
CREATE TABLE pelanggan (
id INTEGER PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
kota VARCHAR(60)
);
CREATE TABLE pesanan (
id INTEGER PRIMARY KEY,
pelanggan_id INTEGER NOT NULL REFERENCES pelanggan(id),
barang VARCHAR(100),
total DECIMAL(12, 2)
);
INSERT INTO pelanggan (id, nama, kota) VALUES (1, 'Budi', 'Jakarta');
SELECT p.id, c.nama, p.barang, p.total
FROM pesanan p
JOIN pelanggan c ON c.id = p.pelanggan_id;Ketika program Java berbicara dengan database, SQL inilah yang dikirim melalui JDBC. Cuplikan berikut memperlihatkan bentuk akhirnya — satu SELECT dieksekusi dan hasilnya digulir baris demi baris:
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:toko.db");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT p.id, c.nama, p.barang FROM pesanan p JOIN pelanggan c ON c.id = p.pelanggan_id")) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " | " + rs.getString("nama"));
}
}Dengan pemahaman tabel, kunci, relasi, dan SQL dasar, kita sudah memegang kosakata database yang dibutuhkan. Pertanyaan berikutnya: bagaimana arsitektur Java menyambungkan kode aplikasi ke database engine yang beragam itu? Chapter berikutnya membongkar lapisan-lapisan JDBC mulai dari DriverManager sampai driver.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
TabelRelasi.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class TabelRelasi {
record Pelanggan(int id, String nama, String kota) {}
record Pesanan(int id, int pelangganId, String barang, long total) {}
record Tag(int id, String label) {}
record PelangganTag(int pelangganId, int tagId) {}
public static void main(String[] args) {
List<Pelanggan> pelanggan = new ArrayList<>(List.of(
new Pelanggan(1, "Budi", "Jakarta"),
new Pelanggan(2, "Siti", "Bandung"),
new Pelanggan(3, "Dewi", "Surabaya")));
List<Pesanan> pesanan = new ArrayList<>(List.of(
new Pesanan(101, 1, "Buku Java", 150_000),
new Pesanan(102, 2, "Mouse", 250_000),
new Pesanan(103, 2, "Keyboard", 450_000)));
System.out.println("== Tabel pelanggan ==");
cetakPelanggan(pelanggan);
// Uji 1: primary key duplikat (id 2 sudah dipakai Siti)
try {
tambahPelanggan(pelanggan, new Pelanggan(2, "Ranto", "Medan"));
} catch (IllegalStateException e) {
System.out.println("DITOLAK: " + e.getMessage());
}
// Uji 2: foreign key tidak valid (pelangganId 99 tidak ada)
try {
tambahPesanan(pelanggan, pesanan, new Pesanan(104, 99, "Monitor", 1_200_000));
} catch (IllegalStateException e) {
System.out.println("DITOLAK: " + e.getMessage());
}
// Uji 3: baris yang sah lolos pemeriksaan kunci
tambahPesanan(pelanggan, pesanan, new Pesanan(104, 3, "Monitor", 1_200_000));
System.out.println("Pesanan 104 tersimpan untuk pelanggan 3 (Dewi).");
// Relasi 1-N: gabungkan pesanan dengan pelanggan pemiliknya
System.out.println();
System.out.println("== Laporan pesanan (gabungan pelanggan <-> pesanan) ==");
for (Pesanan p : pesanan) {
Pelanggan c = cariPelanggan(pelanggan, p.pelangganId());
System.out.printf("%-4d %-6s %-9s %-8s Rp%d%n",
p.id(), c.nama(), p.barang(), c.kota(), p.total());
}
// Relasi N-M: satu pelanggan boleh punya banyak tag, satu tag dipakai
// banyak pelanggan, lewat tabel junction pelanggan_tag
List<Tag> tag = List.of(new Tag(1, "vip"), new Tag(2, "prioritas"));
List<PelangganTag> pelangganTag = List.of(
new PelangganTag(1, 1),
new PelangganTag(1, 2),
new PelangganTag(2, 2),
new PelangganTag(3, 1));
System.out.println();
System.out.println("== Tag pelanggan (relasi N-M lewat tabel junction) ==");
for (Pelanggan c : pelanggan) {
List<String> label = new ArrayList<>();
for (PelangganTag pt : pelangganTag) {
if (pt.pelangganId() == c.id()) {
label.add(cariTag(tag, pt.tagId()).label());
}
}
System.out.printf("%-6s -> %s%n", c.nama(), label);
}
}
static void tambahPelanggan(List<Pelanggan> tabel, Pelanggan baru) {
for (Pelanggan c : tabel) {
if (c.id() == baru.id()) {
throw new IllegalStateException(
"primary key pelanggan.id duplikat: " + baru.id());
}
}
tabel.add(baru);
}
static void tambahPesanan(List<Pelanggan> pelanggan, List<Pesanan> tabel, Pesanan baru) {
if (cariPelanggan(pelanggan, baru.pelangganId()) == null) {
throw new IllegalStateException(
"foreign key pesanan.pelangganId tidak ditemukan: " + baru.pelangganId());
}
tabel.add(baru);
}
static Pelanggan cariPelanggan(List<Pelanggan> tabel, int id) {
for (Pelanggan c : tabel) {
if (c.id() == id) {
return c;
}
}
return null;
}
static Tag cariTag(List<Tag> tabel, int id) {
for (Tag t : tabel) {
if (t.id() == id) {
return t;
}
}
return null;
}
static void cetakPelanggan(List<Pelanggan> tabel) {
System.out.println("| id | nama | kota |");
System.out.println("|----|--------|-----------|");
for (Pelanggan c : tabel) {
System.out.printf("| %-2d | %-6s | %-9s |%n", c.id(), c.nama(), c.kota());
}
}
}