diff --git a/task-1/schema.sql b/task-1/schema.sql index a3b443b..cb9066d 100644 --- a/task-1/schema.sql +++ b/task-1/schema.sql @@ -1 +1,172 @@ --- Write here your DDL statements to create the tables for the database. \ No newline at end of file +-- ============================================================= +-- Library — Database Schema +-- PostgreSQL — 3NF normalised +-- Parent tables first, then children, then N:M bridges. +-- ============================================================= + + +-- ------------------------------------------------------------- +-- publishers +-- A publishing house. One publisher publishes many books (1:N). +-- ------------------------------------------------------------- +CREATE TABLE publishers ( + publisher_id SERIAL PRIMARY KEY, + publisher_name VARCHAR(100) NOT NULL UNIQUE +); + + +-- ------------------------------------------------------------- +-- books +-- A book (the abstract work, not a physical copy). +-- ------------------------------------------------------------- +CREATE TABLE books ( + book_id SERIAL PRIMARY KEY, + book_isbn VARCHAR(13) NOT NULL UNIQUE, + book_title VARCHAR(100) NOT NULL, + publisher_id INT NOT NULL, + + FOREIGN KEY (publisher_id) + REFERENCES publishers (publisher_id) + ON DELETE RESTRICT +); + + +-- ------------------------------------------------------------- +-- authors +-- A single author. Books <-> Authors is N:M (see book_authors). +-- ------------------------------------------------------------- +CREATE TABLE authors ( + author_id SERIAL PRIMARY KEY, + author_name VARCHAR(100) NOT NULL UNIQUE +); + + +-- ------------------------------------------------------------- +-- tags +-- A single tag/topic. Books <-> Tags is N:M (see book_tags). +-- ------------------------------------------------------------- +CREATE TABLE tags ( + tag_id SERIAL PRIMARY KEY, + tag_name VARCHAR(100) NOT NULL UNIQUE +); + + +-- ------------------------------------------------------------- +-- members +-- A person with a library account. +-- ------------------------------------------------------------- +CREATE TABLE members ( + member_id SERIAL PRIMARY KEY, + member_name VARCHAR(100) NOT NULL, + member_email VARCHAR(100) NOT NULL UNIQUE, + member_phone VARCHAR(20) NOT NULL, + member_address VARCHAR(200) NOT NULL +); + + +-- ------------------------------------------------------------- +-- cards +-- A library card. A member has exactly one card (1:1), +-- enforced by UNIQUE on member_id. +-- ------------------------------------------------------------- +CREATE TABLE cards ( + card_id SERIAL PRIMARY KEY, + card_number VARCHAR(20) NOT NULL UNIQUE, + card_issued_on DATE NOT NULL, + card_expires_on DATE NOT NULL, + member_id INT NOT NULL UNIQUE, + + CONSTRAINT chk_card_dates CHECK (card_expires_on > card_issued_on), + + FOREIGN KEY (member_id) + REFERENCES members (member_id) + ON DELETE CASCADE +); + + +-- ------------------------------------------------------------- +-- copies +-- A physical copy of a book. One book has many copies (1:N). +-- ------------------------------------------------------------- +CREATE TABLE copies ( + copy_id SERIAL PRIMARY KEY, + copy_barcode VARCHAR(20) NOT NULL UNIQUE, + shelf_code VARCHAR(20) NOT NULL, + book_id INT NOT NULL, + + FOREIGN KEY (book_id) + REFERENCES books (book_id) + ON DELETE RESTRICT +); + + +-- ------------------------------------------------------------- +-- loans +-- A borrowing event: one member borrows one copy. +-- Has its own data (dates + fine), so it is an entity table, +-- not a pure bridge. +-- ------------------------------------------------------------- +CREATE TABLE loans ( + loan_id SERIAL PRIMARY KEY, + member_id INT NOT NULL, + copy_id INT NOT NULL, + borrowed_at TIMESTAMP NOT NULL, + due_date TIMESTAMP NOT NULL, + returned_at TIMESTAMP, -- NULL = not returned yet + fine_eur NUMERIC(6,2) NOT NULL DEFAULT 0, + + CONSTRAINT chk_fine CHECK (fine_eur >= 0), + CONSTRAINT chk_returned CHECK (returned_at IS NULL OR returned_at >= borrowed_at), + + FOREIGN KEY (member_id) + REFERENCES members (member_id) + ON DELETE RESTRICT, + + FOREIGN KEY (copy_id) + REFERENCES copies (copy_id) + ON DELETE RESTRICT +); + + +-- ------------------------------------------------------------- +-- book_authors +-- N:M bridge between books and authors. +-- UNIQUE (book_id, author_id) prevents duplicate pairings. +-- ------------------------------------------------------------- +CREATE TABLE book_authors ( + book_author_id SERIAL PRIMARY KEY, + book_id INT NOT NULL, + author_id INT NOT NULL, + + UNIQUE (book_id, author_id), + + FOREIGN KEY (book_id) + REFERENCES books (book_id) + ON DELETE CASCADE, + + FOREIGN KEY (author_id) + REFERENCES authors (author_id) + ON DELETE CASCADE +); + + +-- ------------------------------------------------------------- +-- book_tags +-- N:M bridge between books and tags. +-- UNIQUE (book_id, tag_id) prevents duplicate pairings. +-- ------------------------------------------------------------- +CREATE TABLE book_tags ( + book_tag_id SERIAL PRIMARY KEY, + book_id INT NOT NULL, + tag_id INT NOT NULL, + + UNIQUE (book_id, tag_id), + + FOREIGN KEY (book_id) + REFERENCES books (book_id) + ON DELETE CASCADE, + + FOREIGN KEY (tag_id) + REFERENCES tags (tag_id) + ON DELETE CASCADE +); diff --git a/task-2/src/main/java/org/hyf/Database.java b/task-2/src/main/java/org/hyf/Database.java new file mode 100644 index 0000000..10ec30b --- /dev/null +++ b/task-2/src/main/java/org/hyf/Database.java @@ -0,0 +1,17 @@ +package org.hyf; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + + +public class Database { + + private static final String URL = "jdbc:postgresql://localhost:5432/library_db"; + private static final String USER = "hyfuser"; + private static final String PASSWORD = "hyfpassword"; + + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(URL, USER, PASSWORD); + } +} diff --git a/task-2/src/main/java/org/hyf/LibraryRepository.java b/task-2/src/main/java/org/hyf/LibraryRepository.java new file mode 100644 index 0000000..94d9570 --- /dev/null +++ b/task-2/src/main/java/org/hyf/LibraryRepository.java @@ -0,0 +1,173 @@ +package org.hyf; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class LibraryRepository { + + private final Connection conn; + + public LibraryRepository(Connection conn) { + this.conn = conn; + } + + public void insertData() throws SQLException { + // --- publishers (ids 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO publishers (publisher_name) VALUES (?)")) { + ps.setString(1, "O'Reilly"); + ps.executeUpdate(); + ps.setString(1, "Addison-Wesley"); + ps.executeUpdate(); + } + + // --- members (ids 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO members (member_name, member_email, member_phone, member_address) VALUES (?, ?, ?, ?)")) { + ps.setString(1, "Tom"); + ps.setString(2, "tom@example.com"); + ps.setString(3, "+31645678901"); + ps.setString(4, "Biltstraat 88, Utrecht"); + ps.executeUpdate(); + ps.setString(1, "Sam"); + ps.setString(2, "sam@example.com"); + ps.setString(3, "+31612345678"); + ps.setString(4, "Oudegracht 10, Utrecht"); + ps.executeUpdate(); + } + + // --- authors (ids 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO authors (author_name) VALUES (?)")) { + ps.setString(1, "Douglas Crockford"); + ps.executeUpdate(); + ps.setString(1, "Joshua Bloch"); + ps.executeUpdate(); + } + + // --- tags (ids 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO tags (tag_name) VALUES (?)")) { + ps.setString(1, "javascript"); + ps.executeUpdate(); + ps.setString(1, "java"); + ps.executeUpdate(); + } + + // --- books (ids 1, 2; publisher_id -> publishers 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO books (book_isbn, book_title, publisher_id) VALUES (?, ?, ?)")) { + ps.setString(1, "9780596517748"); + ps.setString(2, "JavaScript: The Good Parts"); + ps.setInt(3, 1); + ps.executeUpdate(); + ps.setString(1, "9780134685991"); + ps.setString(2, "Effective Java"); + ps.setInt(3, 2); + ps.executeUpdate(); + } + + // --- cards (member_id -> members 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO cards (card_number, card_issued_on, card_expires_on, member_id) VALUES (?, ?, ?, ?)")) { + ps.setString(1, "C-1004"); + ps.setDate(2, Date.valueOf("2023-11-20")); + ps.setDate(3, Date.valueOf("2026-11-20")); + ps.setInt(4, 1); + ps.executeUpdate(); + ps.setString(1, "C-1001"); + ps.setDate(2, Date.valueOf("2024-01-10")); + ps.setDate(3, Date.valueOf("2027-01-10")); + ps.setInt(4, 2); + ps.executeUpdate(); + } + + // --- copies (ids 1, 2; book_id -> books 1, 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO copies (copy_barcode, shelf_code, book_id) VALUES (?, ?, ?)")) { + ps.setString(1, "BC-0011"); + ps.setString(2, "B-03"); + ps.setInt(3, 1); + ps.executeUpdate(); + ps.setString(1, "BC-0001"); + ps.setString(2, "A-01"); + ps.setInt(3, 2); + ps.executeUpdate(); + } + + // --- loans (member_id + copy_id) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO loans (member_id, copy_id, borrowed_at, due_date, returned_at, fine_eur) VALUES (?, ?, ?, ?, ?, ?)")) { + ps.setInt(1, 1); + ps.setInt(2, 1); + ps.setTimestamp(3, Timestamp.valueOf("2025-12-02 08:08:00")); + ps.setTimestamp(4, Timestamp.valueOf("2025-12-23 08:08:00")); + ps.setTimestamp(5, Timestamp.valueOf("2025-12-13 08:08:00")); + ps.setBigDecimal(6, new BigDecimal("0.00")); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setInt(2, 2); + ps.setTimestamp(3, Timestamp.valueOf("2026-03-09 03:18:00")); + ps.setTimestamp(4, Timestamp.valueOf("2026-03-30 03:18:00")); + ps.setTimestamp(5, Timestamp.valueOf("2026-03-28 03:18:00")); + ps.setBigDecimal(6, new BigDecimal("1.25")); + ps.executeUpdate(); + } + + // --- book_authors bridge (book 1 + author 1, book 2 + author 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO book_authors (book_id, author_id) VALUES (?, ?)")) { + ps.setInt(1, 1); + ps.setInt(2, 1); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setInt(2, 2); + ps.executeUpdate(); + } + + // --- book_tags bridge (book 1 + tag 1, book 2 + tag 2) --- + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO book_tags (book_id, tag_id) VALUES (?, ?)")) { + ps.setInt(1, 1); + ps.setInt(2, 1); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setInt(2, 2); + ps.executeUpdate(); + } + } + + /** + * Fetches all borrowings and returns them as a list. + * Uses joins across loans -> members -> copies -> books. + */ + public List fetchData() throws SQLException { + String sql = + "SELECT m.member_name, b.book_title, c.copy_barcode, l.borrowed_at, l.returned_at " + + "FROM loans l " + + "JOIN members m ON m.member_id = l.member_id " + + "JOIN copies c ON c.copy_id = l.copy_id " + + "JOIN books b ON b.book_id = c.book_id " + + "ORDER BY l.borrowed_at"; + + List result = new ArrayList<>(); + try (PreparedStatement ps = conn.prepareStatement(sql); + ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + result.add(rs.getString("member_name") + + " -> " + rs.getString("book_title") + + " (copy " + rs.getString("copy_barcode") + ")" + + ", borrowed " + rs.getTimestamp("borrowed_at") + + ", returned " + rs.getTimestamp("returned_at")); + } + } + return result; + } +} diff --git a/task-2/src/main/java/org/hyf/Main.java b/task-2/src/main/java/org/hyf/Main.java index 7be95f2..c55da9b 100644 --- a/task-2/src/main/java/org/hyf/Main.java +++ b/task-2/src/main/java/org/hyf/Main.java @@ -1,7 +1,33 @@ package org.hyf; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Entry point. Wires the pieces together: + * get a connection from Database, hand it to LibraryRepository, + * then print what fetchData() returns. + */ public class Main { + public static void main(String[] args) { - System.out.println("You code goes here :) You can overwrite this entire method."); + // The connection is opened once here and shared with the repository. + try (Connection conn = Database.getConnection()) { + System.out.println("Connected! " + conn.getCatalog()); + + LibraryRepository repo = new LibraryRepository(conn); + + repo.insertData(); + System.out.println("\nData inserted.\n"); + + List borrowings = repo.fetchData(); + System.out.println("Current borrowings (member -> book):"); + for (String row : borrowings) { + System.out.println(" " + row); + } + } catch (SQLException e) { + e.printStackTrace(); + } } } diff --git a/task-2/target/classes/org/hyf/Database.class b/task-2/target/classes/org/hyf/Database.class new file mode 100644 index 0000000..a850526 Binary files /dev/null and b/task-2/target/classes/org/hyf/Database.class differ diff --git a/task-2/target/classes/org/hyf/LibraryRepository.class b/task-2/target/classes/org/hyf/LibraryRepository.class new file mode 100644 index 0000000..70c5a2d Binary files /dev/null and b/task-2/target/classes/org/hyf/LibraryRepository.class differ diff --git a/task-2/target/classes/org/hyf/Main.class b/task-2/target/classes/org/hyf/Main.class new file mode 100644 index 0000000..fe97efd Binary files /dev/null and b/task-2/target/classes/org/hyf/Main.class differ