Sebelas chapter sebelumnya memperlajari setiap komponen desain satu per satu: entitas, relasi, junction, normalisasi, sampai schema. Sekarang semuanya digunakan sekaligus. Chapter penutup ini menjalankan proses perancangan dari awal sampai akhir — dari satu paragraf kebutuhan pengguna menjadi sekumpulan tabel yang siap diquery — dengan perpustakaan kecil sebagai studi kasus. Ikuti urutan langkahnya, karena urutan itulah yang membuat keputusan desain bisa dilacak dan dipertanggungjawabkan.
Langkah 1: Narasi Kebutuhan #
Semua desain dimulai dari kebutuhan yang ditulis pengguna, bukan dari tabel. Berikut narasi dari pengelola perpustakaan:
“Anggota mendaftar dengan nama dan email, lalu mendapat nomor kartu. Setiap anggota boleh meminjam buku beberapa kali. Sekali pinjam bisa membawa beberapa buku sekaligus, dengan satu tanggal jatuh tempo. Sebuah buku ditulis oleh satu atau beberapa penulis. Kami perlu tahu buku mana yang sedang tidak di rak, siapa yang terlambat, buku apa yang paling sering dipinjam, dan daftar karya satu penulis.”
Narasi ini sengaja pendek tetapi memuat lima kebutuhan query yang berbeda. Skema dikatakan baik bukan karena cantik di atas kertas, melainkan karena kelima kebutuhan itu bisa dijawab tanpa perjuangan — uji yang akan kita jalankan di langkah terakhir.
Langkah 2: Mengekstrak Entitas dan Atribut #
Kata benda yang berulang dan penting menjadi kandidat entitas; informasi yang mendeskripsikannya menjadi atribut. Hasil penerapan itu pada narasi:
| Kata benda di narasi | Entitas | Atribut kunci |
|---|---|---|
| anggota, nama, email, nomor kartu | anggota | nama, email (unik), tanggal gabung |
| buku, judul | buku | isbn (unik), judul, tahun terbit |
| penulis | penulis | nama |
| meminjam beberapa kali | peminjaman | anggota, tanggal pinjam, jatuh tempo |
| beberapa buku sekaligus | item_peminjaman | peminjaman, buku, tanggal kembali |
Perhatikan bahwa “meminjam” dan “beberapa buku sekaligus” tidak hilang sebagai kata kerja — keduanya menjadi entitas karena menyimpan fakta sendiri. Kalimat “buku ditulis beberapa penulis” juga menyimpan fakta relasi, dan akan kita tangani di langkah berikutnya.
Langkah 3: Menentukan Relasi dan Kardinalitas #
Setiap pasangan entitas yang saling menyebut diperiksa kardinalitasnya. Hasilnya untuk skema ini:
| Relasi | Kardinalitas | Implementasi |
|---|---|---|
anggota — peminjaman | satu ke banyak | FK id_anggota di tabel peminjaman |
peminjaman — buku | banyak ke banyak (dengan atribut tanggal_kembali) | junction item_peminjaman |
penulis — buku | banyak ke banyak (murni) | junction buku_penulis |
Dua keputusan penting lahir di tahap ini. Pertama, tanggal kembali dipastikan bukan atribut buku (buku dipinjam berkali-kali) dan bukan atribut peminjaman (satu peminjaman membawa beberapa buku yang bisa kembali beda waktu) — ia milik pasangan keduanya, sehingga menempel di junction item_peminjaman. Kedua, relasi penulis-buku memang M:N murni sejak narasi menyebut “satu atau beberapa penulis”, jadi tidak ada jalan selain junction.
Langkah 4: Kunci dan Constraint #
Setiap tabel membutuhkan identitas. Kami memilih surrogate key integer GENERATED ALWAYS AS IDENTITY untuk semua tabel utama — nomor kartu, isbn, dan email tetap disimpan sebagai kolom bisnis dengan UNIQUE, tetapi primary key tetap bilangan bulat yang stabil walau nomor kartu berganti format. Tiga aturan tambahan ikut ditegakkan: isbn unik, email anggota unik, dan tanggal_jatuh_tempo > tanggal_pinjam lewat CHECK. Hasil akhirnya sebagai berikut:
-- Hasil akhir langkah 1-4: lima tabel yang masing-masing punya satu tugas.
CREATE TABLE penulis (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
nama text NOT NULL
);
CREATE TABLE buku (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
isbn text NOT NULL UNIQUE,
judul text NOT NULL,
tahun_terbit integer NOT NULL CHECK (tahun_terbit BETWEEN 1900 AND 2026)
);
-- Junction M:N: satu buku boleh ditulis banyak penulis,
-- satu penulis boleh menulis banyak buku.
CREATE TABLE buku_penulis (
id_buku integer NOT NULL REFERENCES buku (id) ON DELETE CASCADE,
id_penulis integer NOT NULL REFERENCES penulis (id) ON DELETE CASCADE,
PRIMARY KEY (id_buku, id_penulis)
);
CREATE TABLE anggota (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
tanggal_gabung date NOT NULL
);
-- Satu peminjaman membawa banyak buku lewat item_peminjaman.
CREATE TABLE peminjaman (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
id_anggota integer NOT NULL REFERENCES anggota (id),
tanggal_pinjam date NOT NULL,
tanggal_jatuh_tempo date NOT NULL,
CHECK (tanggal_jatuh_tempo > tanggal_pinjam)
);
-- Junction dengan atribut tambahan: tanggal_kembali milik PASANGAN
-- peminjaman-buku, bukan milik peminjaman maupun milik buku.
CREATE TABLE item_peminjaman (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
id_peminjaman integer NOT NULL REFERENCES peminjaman (id) ON DELETE CASCADE,
id_buku integer NOT NULL REFERENCES buku (id),
tanggal_kembali date,
CHECK (tanggal_kembali IS NULL OR tanggal_kembali >= DATE '2026-01-01')
);Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLEDua catatan pada definisi di atas. CHECK pada item_peminjaman hanya bisa memeriksa kolom tabelnya sendiri — aturan “tanggal kembali tidak boleh sebelum tanggal pinjam” melibatkan tabel peminjaman, dan CHECK lintas tabel memang tidak didukung PostgreSQL (pembahasan chapter Check Constraint); untuk latihan ini cukup validasi di sisi aplikasi. ON DELETE CASCADE dipilih untuk buku_penulis dan item_peminjaman karena menghapus buku atau peminjaman memang harus menghapus catatan relasinya, sementara menghapus anggota memakai NO ACTION agar riwayat pinjam tidak ikut lenyap diam-diam.
Langkah 5: Data Uji yang Mewakili Semua Kasus #
Skema baru layak diuji dengan data yang sengaja memuat kasus-kasus sulit: buku berpenulis ganda, peminjaman yang sudah selesai, yang masih berjalan, dan yang terlambat.
-- Data uji: ukuran kecil, tapi setiap kebutuhan punya kasusnya.
-- Ada buku berpenulis ganda, peminjaman yang sudah kembali,
-- peminjaman yang masih berjalan, dan peminjaman yang terlambat.
INSERT INTO penulis (nama) VALUES
('Ayu Prameswari'),
('Bintang Sanjaya'),
('Citra Maharani');
INSERT INTO buku (isbn, judul, tahun_terbit) VALUES
('978-602-0001-01-8', 'Panduan Berkebun Sayur', 2023),
('978-602-0002-02-2', 'Matahari Pagi', 2021),
('978-602-0003-03-9', 'Memasak untuk Pemula', 2022),
('978-602-0004-04-6', 'Puisi Hujan', 2020);
-- Buku 1 ditulis dua penulis sekaligus -- bukti relasi M:N hidup.
INSERT INTO buku_penulis (id_buku, id_penulis) VALUES
(1, 1),
(1, 2),
(2, 3),
(3, 2),
(4, 3);
INSERT INTO anggota (nama, email, tanggal_gabung) VALUES
('Budi Santoso', '[email protected]', DATE '2025-11-02'),
('Siti Rahayu', '[email protected]', DATE '2025-12-15'),
('Dewi Lestari', '[email protected]', DATE '2026-01-20');
INSERT INTO peminjaman (id_anggota, tanggal_pinjam, tanggal_jatuh_tempo) VALUES
(1, DATE '2026-02-01', DATE '2026-02-15'),
(2, DATE '2026-02-05', DATE '2026-02-19'),
(3, DATE '2026-03-01', DATE '2026-03-15'),
(1, DATE '2026-03-05', DATE '2026-03-19'),
(2, DATE '2026-03-10', DATE '2026-03-24');
INSERT INTO item_peminjaman (id_peminjaman, id_buku, tanggal_kembali) VALUES
(1, 1, DATE '2026-02-10'),
(1, 2, DATE '2026-02-12'),
(2, 1, DATE '2026-02-14'),
(3, 1, NULL),
(3, 4, NULL),
(4, 3, NULL),
(5, 2, NULL);
-- Peta data: peminjaman 1-2 sudah selesai, 3-5 masih berjalan;
-- per 2026-03-20, peminjaman 3 dan 4 melewati jatuh tempo.Output yang Dihasilkan:
INSERT 0 3
INSERT 0 4
INSERT 0 5
INSERT 0 3
INSERT 0 5
INSERT 0 7Data ini kecil — hanya tiga penulis, empat buku, tiga anggota, lima peminjaman — tetapi setiap kebutuhan narasi punya kasusnya: ada buku oleh dua penulis (Panduan Berkebun Sayur), peminjaman selesai dan berjalan, serta dua peminjaman yang melewati jatuh tempo per tanggal acuan 2026-03-20.
Langkah 6: Membuktikan dengan Query #
Inilah ujian sesungguhnya: lima kebutuhan dari narasi dijawab lima query. Perhatikan bahwa tidak ada satu pun query yang perlu membedah string, menebak kolom berulang, atau join ganda yang ambigu:
-- Ujian akhir desain: satu query untuk setiap kebutuhan narasi.
-- Skema yang benar menjawab semua kebutuhan tanpa operasi pembedah string,
-- tanpa join ganda, tanpa asumsi yang tidak tertulis.
-- Kebutuhan 1: buku apa saja yang sedang tidak ada di rak, dan siapa
-- yang memegangnya? (tanggal_kembali NULL = masih dipinjam)
SELECT b.judul, a.nama AS pemegang, pm.tanggal_jatuh_tempo
FROM item_peminjaman i
JOIN peminjaman pm ON pm.id = i.id_peminjaman
JOIN anggota a ON a.id = pm.id_anggota
JOIN buku b ON b.id = i.id_buku
WHERE i.tanggal_kembali IS NULL
ORDER BY b.judul, a.nama;
-- Kebutuhan 2: siapa saja yang terlambat mengembalikan per 2026-03-20?
-- Terlambat = belum kembali DAN melewati jatuh tempo.
SELECT a.nama, b.judul, pm.tanggal_jatuh_tempo
FROM item_peminjaman i
JOIN peminjaman pm ON pm.id = i.id_peminjaman
JOIN anggota a ON a.id = pm.id_anggota
JOIN buku b ON b.id = i.id_buku
WHERE i.tanggal_kembali IS NULL
AND pm.tanggal_jatuh_tempo < DATE '2026-03-20'
ORDER BY a.nama, b.judul;
-- Kebutuhan 3: buku terpopuler dihitung dari riwayat peminjaman.
SELECT b.judul, count(*) AS kali_dipinjam
FROM item_peminjaman i
JOIN buku b ON b.id = i.id_buku
GROUP BY b.id, b.judul
ORDER BY count(*) DESC, b.judul;
-- Kebutuhan 4: daftar karya seorang penulis -- relasi M:N dibaca
-- lewat junction tanpa perlu tahu siapa penulis pertamanya.
SELECT b.judul, b.tahun_terbit
FROM buku_penulis bp
JOIN buku b ON b.id = bp.id_buku
JOIN penulis pn ON pn.id = bp.id_penulis
WHERE pn.nama = 'Citra Maharani'
ORDER BY b.tahun_terbit;
-- Kebutuhan 5: riwayat peminjaman satu anggota, buku yang belum
-- kembali tetap tampil dengan kolom tanggal_kembali kosong.
SELECT b.judul, pm.tanggal_pinjam, i.tanggal_kembali
FROM item_peminjaman i
JOIN peminjaman pm ON pm.id = i.id_peminjaman
JOIN anggota a ON a.id = pm.id_anggota
JOIN buku b ON b.id = i.id_buku
WHERE a.nama = 'Budi Santoso'
ORDER BY pm.tanggal_pinjam, b.judul;Output yang Dihasilkan:
judul | pemegang | tanggal_jatuh_tempo
------------------------+--------------+---------------------
Matahari Pagi | Siti Rahayu | 2026-03-24
Memasak untuk Pemula | Budi Santoso | 2026-03-19
Panduan Berkebun Sayur | Dewi Lestari | 2026-03-15
Puisi Hujan | Dewi Lestari | 2026-03-15
(4 rows)
nama | judul | tanggal_jatuh_tempo
--------------+------------------------+---------------------
Budi Santoso | Memasak untuk Pemula | 2026-03-19
Dewi Lestari | Panduan Berkebun Sayur | 2026-03-15
Dewi Lestari | Puisi Hujan | 2026-03-15
(3 rows)
judul | kali_dipinjam
------------------------+---------------
Panduan Berkebun Sayur | 3
Matahari Pagi | 2
Memasak untuk Pemula | 1
Puisi Hujan | 1
(4 rows)
judul | tahun_terbit
---------------+--------------
Puisi Hujan | 2020
Matahari Pagi | 2021
(2 rows)
judul | tanggal_pinjam | tanggal_kembali
------------------------+----------------+-----------------
Matahari Pagi | 2026-02-01 | 2026-02-12
Panduan Berkebun Sayur | 2026-02-01 | 2026-02-10
Memasak untuk Pemula | 2026-03-05 |
(3 rows)Bacalah hasil keenam pernyataan di output dan cocokkan dengan narasi: kebutuhan pertama mengembalikan empat buku yang sedang dipinjam lengkap dengan pemegangnya; kebutuhan kedua menunjuk dua peminjaman terlambat (Dewi dan Budi); kebutuhan ketiga menempatkan Panduan Berkebun Sayur dan Puisi Hujan sama-sama dipinjam dua kali; kebutuhan keempat menghasilkan dua karya Citra Maharani; dan kebutuhan kelima merangkum riwayat Budi dengan satu baris yang kolom tanggal kembalinya kosong. Semua itu mungkin karena setiap fakta menempel di tabel yang tepat.
Langkah 7: Menambah Index untuk Jalur yang Sibuk #
Primary key dan UNIQUE otomatis membuat index, tetapi kolom foreign key tidak — index FK harus dibuat sendiri untuk jalur join yang sering dipakai:
-- Lapisan performa: PK dan UNIQUE otomatis membuat index,
-- tetapi kolom foreign key TIDAK -- kita buat sendiri untuk dua
-- jalur join yang paling sering dipakai.
CREATE INDEX idx_item_buku ON item_peminjaman (id_buku);
CREATE INDEX idx_peminjaman_anggota ON peminjaman (id_anggota);
-- Peta index akhir: index _pkey dan _key lahir otomatis dari
-- PRIMARY KEY serta UNIQUE; index idx_* adalah dua yang kita tambahkan.
SELECT tablename, indexname
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;Output yang Dihasilkan:
CREATE INDEX
CREATE INDEX
tablename | indexname
-----------------+------------------------
anggota | anggota_email_key
anggota | anggota_pkey
buku | buku_isbn_key
buku | buku_pkey
buku_penulis | buku_penulis_pkey
item_peminjaman | idx_item_buku
item_peminjaman | item_peminjaman_pkey
peminjaman | idx_peminjaman_anggota
peminjaman | peminjaman_pkey
penulis | penulis_pkey
(10 rows)Daftar index pada output memperlihatkan pola yang bisa diprediksi: setiap tabel punya index _pkey, kolom isbn dan email mendapat index _key dari constraint UNIQUE, dan hanya dua index idx_* yang benar-benar kita tulis sendiri. Kapan masing-masing jenis index lebih tepat — dan kapan index justru memperlambat — adalah materi satu section penuh berikutnya.
Daftar Periksa Perancangan #
Proses tujuh langkah di atas bisa diringkas menjadi daftar periksa yang dapat diulang untuk kasus lain:
| Langkah | Pertanyaan kunci | Output |
|---|---|---|
| 1. Narasi | Kebutuhan query apa saja yang harus terjawab? | Daftar kebutuhan |
| 2. Entitas | Kata benda apa yang menyimpan fakta sendiri? | Tabel + atribut |
| 3. Relasi | Berapa kardinalitas tiap pasangan entitas? | FK dan junction |
| 4. Kunci | Apa identitas stabil tiap tabel? | PK, UNIQUE, CHECK |
| 5. Data uji | Apakah semua kasus sulit terwakili? | Data kecil lengkap |
| 6. Bukti | Apakah tiap kebutuhan terjawab satu query? | Query + output |
| 7. Index | Jalur join mana yang paling sibuk? | Index FK |
Dengan skema yang kini teruji, perjalanan belajar kita berpindah dari bagaimana merancang ke bagaimana mempercepat: Section Indexes akan membedah struktur data di balik CREATE INDEX — dimulai dari konsep index di chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
data-uji.sql
Unduh berkas-- Data uji: ukuran kecil, tapi setiap kebutuhan punya kasusnya.
-- Ada buku berpenulis ganda, peminjaman yang sudah kembali,
-- peminjaman yang masih berjalan, dan peminjaman yang terlambat.
INSERT INTO penulis (nama) VALUES
('Ayu Prameswari'),
('Bintang Sanjaya'),
('Citra Maharani');
INSERT INTO buku (isbn, judul, tahun_terbit) VALUES
('978-602-0001-01-8', 'Panduan Berkebun Sayur', 2023),
('978-602-0002-02-2', 'Matahari Pagi', 2021),
('978-602-0003-03-9', 'Memasak untuk Pemula', 2022),
('978-602-0004-04-6', 'Puisi Hujan', 2020);
-- Buku 1 ditulis dua penulis sekaligus -- bukti relasi M:N hidup.
INSERT INTO buku_penulis (id_buku, id_penulis) VALUES
(1, 1),
(1, 2),
(2, 3),
(3, 2),
(4, 3);
INSERT INTO anggota (nama, email, tanggal_gabung) VALUES
('Budi Santoso', '[email protected]', DATE '2025-11-02'),
('Siti Rahayu', '[email protected]', DATE '2025-12-15'),
('Dewi Lestari', '[email protected]', DATE '2026-01-20');
INSERT INTO peminjaman (id_anggota, tanggal_pinjam, tanggal_jatuh_tempo) VALUES
(1, DATE '2026-02-01', DATE '2026-02-15'),
(2, DATE '2026-02-05', DATE '2026-02-19'),
(3, DATE '2026-03-01', DATE '2026-03-15'),
(1, DATE '2026-03-05', DATE '2026-03-19'),
(2, DATE '2026-03-10', DATE '2026-03-24');
INSERT INTO item_peminjaman (id_peminjaman, id_buku, tanggal_kembali) VALUES
(1, 1, DATE '2026-02-10'),
(1, 2, DATE '2026-02-12'),
(2, 1, DATE '2026-02-14'),
(3, 1, NULL),
(3, 4, NULL),
(4, 3, NULL),
(5, 2, NULL);
-- Peta data: peminjaman 1-2 sudah selesai, 3-5 masih berjalan;
-- per 2026-03-20, peminjaman 3 dan 4 melewati jatuh tempo.
indeks-pendukung.sql
Unduh berkas-- Lapisan performa: PK dan UNIQUE otomatis membuat index,
-- tetapi kolom foreign key TIDAK -- kita buat sendiri untuk dua
-- jalur join yang paling sering dipakai.
CREATE INDEX idx_item_buku ON item_peminjaman (id_buku);
CREATE INDEX idx_peminjaman_anggota ON peminjaman (id_anggota);
-- Peta index akhir: index _pkey dan _key lahir otomatis dari
-- PRIMARY KEY serta UNIQUE; index idx_* adalah dua yang kita tambahkan.
SELECT tablename, indexname
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;
query-kebutuhan.sql
Unduh berkas-- Ujian akhir desain: satu query untuk setiap kebutuhan narasi.
-- Skema yang benar menjawab semua kebutuhan tanpa operasi pembedah string,
-- tanpa join ganda, tanpa asumsi yang tidak tertulis.
-- Kebutuhan 1: buku apa saja yang sedang tidak ada di rak, dan siapa
-- yang memegangnya? (tanggal_kembali NULL = masih dipinjam)
SELECT b.judul, a.nama AS pemegang, pm.tanggal_jatuh_tempo
FROM item_peminjaman i
JOIN peminjaman pm ON pm.id = i.id_peminjaman
JOIN anggota a ON a.id = pm.id_anggota
JOIN buku b ON b.id = i.id_buku
WHERE i.tanggal_kembali IS NULL
ORDER BY b.judul, a.nama;
-- Kebutuhan 2: siapa saja yang terlambat mengembalikan per 2026-03-20?
-- Terlambat = belum kembali DAN melewati jatuh tempo.
SELECT a.nama, b.judul, pm.tanggal_jatuh_tempo
FROM item_peminjaman i
JOIN peminjaman pm ON pm.id = i.id_peminjaman
JOIN anggota a ON a.id = pm.id_anggota
JOIN buku b ON b.id = i.id_buku
WHERE i.tanggal_kembali IS NULL
AND pm.tanggal_jatuh_tempo < DATE '2026-03-20'
ORDER BY a.nama, b.judul;
-- Kebutuhan 3: buku terpopuler dihitung dari riwayat peminjaman.
SELECT b.judul, count(*) AS kali_dipinjam
FROM item_peminjaman i
JOIN buku b ON b.id = i.id_buku
GROUP BY b.id, b.judul
ORDER BY count(*) DESC, b.judul;
-- Kebutuhan 4: daftar karya seorang penulis -- relasi M:N dibaca
-- lewat junction tanpa perlu tahu siapa penulis pertamanya.
SELECT b.judul, b.tahun_terbit
FROM buku_penulis bp
JOIN buku b ON b.id = bp.id_buku
JOIN penulis pn ON pn.id = bp.id_penulis
WHERE pn.nama = 'Citra Maharani'
ORDER BY b.tahun_terbit;
-- Kebutuhan 5: riwayat peminjaman satu anggota, buku yang belum
-- kembali tetap tampil dengan kolom tanggal_kembali kosong.
SELECT b.judul, pm.tanggal_pinjam, i.tanggal_kembali
FROM item_peminjaman i
JOIN peminjaman pm ON pm.id = i.id_peminjaman
JOIN anggota a ON a.id = pm.id_anggota
JOIN buku b ON b.id = i.id_buku
WHERE a.nama = 'Budi Santoso'
ORDER BY pm.tanggal_pinjam, b.judul;
skema-final.sql
Unduh berkas-- Hasil akhir langkah 1-4: lima tabel yang masing-masing punya satu tugas.
CREATE TABLE penulis (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
nama text NOT NULL
);
CREATE TABLE buku (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
isbn text NOT NULL UNIQUE,
judul text NOT NULL,
tahun_terbit integer NOT NULL CHECK (tahun_terbit BETWEEN 1900 AND 2026)
);
-- Junction M:N: satu buku boleh ditulis banyak penulis,
-- satu penulis boleh menulis banyak buku.
CREATE TABLE buku_penulis (
id_buku integer NOT NULL REFERENCES buku (id) ON DELETE CASCADE,
id_penulis integer NOT NULL REFERENCES penulis (id) ON DELETE CASCADE,
PRIMARY KEY (id_buku, id_penulis)
);
CREATE TABLE anggota (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
tanggal_gabung date NOT NULL
);
-- Satu peminjaman membawa banyak buku lewat item_peminjaman.
CREATE TABLE peminjaman (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
id_anggota integer NOT NULL REFERENCES anggota (id),
tanggal_pinjam date NOT NULL,
tanggal_jatuh_tempo date NOT NULL,
CHECK (tanggal_jatuh_tempo > tanggal_pinjam)
);
-- Junction dengan atribut tambahan: tanggal_kembali milik PASANGAN
-- peminjaman-buku, bukan milik peminjaman maupun milik buku.
CREATE TABLE item_peminjaman (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
id_peminjaman integer NOT NULL REFERENCES peminjaman (id) ON DELETE CASCADE,
id_buku integer NOT NULL REFERENCES buku (id),
tanggal_kembali date,
CHECK (tanggal_kembali IS NULL OR tanggal_kembali >= DATE '2026-01-01')
);