Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 172 additions & 1 deletion task-1/schema.sql
Original file line number Diff line number Diff line change
@@ -1 +1,172 @@
-- Write here your DDL statements to create the tables for the database.
-- =============================================================
-- 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).
-- -------------------------------------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on the schema!
The formatting is very clean and consistent, it looks like you set up a proper SQL formatter with the PostgreSQL dialect. The clear section headers, thoughtful comments, and logical table ordering make the file easy to read and professional. This level of care really stands out. Well done!

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 (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema is already readable and the table comments help a lot. What do you think about arranging the table definitions so the reading flow follows the domain while still keeping parent tables before dependent tables.
Then you could have:

members
cards
publishers
authors
tags
books
book_authors
book_tags
copies
loans

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIMESTAMP stores a date-time without timezone information. That is fine for a local demo, but for real applications TIMESTAMPTZ is often safer for events such as borrowed_at, due_date, and returned_at, because it stores an absolute moment in time by converting the value to UTC. Then any library in different timezones using an implementation like LocalDateTime can calculate the offset and show the correct time relevant to it.

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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An extra constraint to add would be to check that due date is after borrowed date.

CONSTRAINT chk_due_date CHECK (due_date > 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
);
17 changes: 17 additions & 0 deletions task-2/src/main/java/org/hyf/Database.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.hyf;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;


public class Database {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice split into separate files. The use of LibraryRepository is a good naming choice: in applications, the layer that talks to the database is usually called a Repository layer, similar to how we used Controller -> Service -> Client when calling an external API.

One small naming improvement: Database currently only creates the JDBC connection, so a more precise name could be DatabaseConnection, ConnectionFactory, or DatabaseConfig. That would make the responsibility of the file clearer at a glance.


private static final String URL = "jdbc:postgresql://localhost:5432/library_db";
private static final String USER = "hyfuser";
private static final String PASSWORD = "hyfpassword";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can understand hardcoded local credentials in an assignment. Just a hint, the next professional step is to read connection details from environment variables so secrets do not live in source code:

private static final String URL = System.getenv().getOrDefault(
        "DB_URL",
        "jdbc:postgresql://localhost:5432/library_db"
);
private static final String USER = System.getenv().getOrDefault("DB_USER", "hyfuser");
private static final String PASSWORD = System.getenv("DB_PASSWORD");


public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
173 changes: 173 additions & 0 deletions task-2/src/main/java/org/hyf/LibraryRepository.java
Original file line number Diff line number Diff line change
@@ -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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent rows are inserted correctly, but the later inserts rely on magic numbers for foreign keys. For example, after inserting publishers, the book inserts use publisher_id values 1 and 2 directly. This assumes a fresh DB but on subsequent runs or deleted rows, id could be different and the book could be assigned to the wrong publisher.

A stronger approach is to store the generated IDs from the parent inserts, then use those variables when inserting related rows. That also makes the code easier to read because oreillyPublisherId explains the meaning better than 1.

int oreillyPublisherId;

try (PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO publishers (publisher_name) VALUES (?) RETURNING publisher_id")) {
    ps.setString(1, "O'Reilly");

    try (ResultSet rs = ps.executeQuery()) {
        rs.next();
        oreillyPublisherId = rs.getInt("publisher_id");
    }
}

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, oreillyPublisherId);
    ps.executeUpdate();
}

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"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small modern Java/PostgreSQL improvement: the code currently uses java.sql.Date.valueOf(...) and java.sql.Timestamp.valueOf(...), which works for this assignment. In newer Java code, it is more common to model dates and times with LocalDate, LocalDateTime, or OffsetDateTime, then pass them to JDBC with setObject(...).

Please see my comment also related to this on TIMESTAMP in the schema.

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<String> 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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend using a text block for you SQL statements which can be cleaner:

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<String> 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;
}
}
Loading