-
Notifications
You must be signed in to change notification settings - Fork 6
R. Yusup #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
R. Yusup #3
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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). | ||
| -- ------------------------------------------------------------- | ||
| 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 ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| 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 | ||
| ); | ||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice split into separate files. The use of One small naming improvement: |
||
|
|
||
| private static final String URL = "jdbc:postgresql://localhost:5432/library_db"; | ||
| private static final String USER = "hyfuser"; | ||
| private static final String PASSWORD = "hyfpassword"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| 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"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 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")); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One small modern Java/PostgreSQL improvement: the code currently uses Please see my comment also related to this on |
||
| 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"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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!