Skip to content

R. Yusup#3

Open
Yusuprozimemet wants to merge 1 commit into
HackYourAssignment:mainfrom
Yusuprozimemet:main
Open

R. Yusup#3
Yusuprozimemet wants to merge 1 commit into
HackYourAssignment:mainfrom
Yusuprozimemet:main

Conversation

@Yusuprozimemet

@Yusuprozimemet Yusuprozimemet commented Jun 3, 2026

Copy link
Copy Markdown

Add library database schema and JDBC CRUD demo

Task 1: Normalize the library export into a 3NF PostgreSQL schema
(schema.sql) with 10 tables, covering 1:1 (members-cards),
1:N (publishers-books, books-copies, members-loans), and two N:M
relationships via bridge tables (book_authors, book_tags). Includes
PK/FK/UNIQUE/CHECK constraints.

Task 2: Add a JDBC console demo (Main, Database, LibraryRepository)
that connects to library_db, inserts two rows per table with
PreparedStatement, and fetches borrowings via a multi-table JOIN.

@Yusuprozimemet Yusuprozimemet changed the title Add 3NF library schema (Task 1) and JDBC CRUD demo (Task 2) R. Yusup Jun 3, 2026

@cometbroom cometbroom left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a very strong submission. The schema is well-designed, properly normalized to 3NF, and clearly exceeds the minimum requirements. Clearly you've thought through both the structure and the presentation.
However, the target/ folder (Maven build output) was included in the submission. This is a common oversight, but it’s worth addressing because these kinds of small packaging mistakes can unintentionally reduce the professional impression of otherwise excellent work.
For future submissions (both in this course and beyond), I recommend setting up a simple checklist or using a .gitignore template for Maven/Java projects. You could automate a lot of that :)

Comment thread task-1/schema.sql
-- -------------------------------------------------------------
-- 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!

Comment thread task-1/schema.sql
-- 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

Comment thread task-1/schema.sql
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)

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

// --- 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();
}

"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.

Comment thread task-1/schema.sql
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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants