-
Notifications
You must be signed in to change notification settings - Fork 6
Monerh A #4
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?
Monerh A #4
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,71 @@ | ||
| -- Write here your DDL statements to create the tables for the database. | ||
| CREATE TABLE members ( | ||
| member_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| member_email TEXT NOT NULL UNIQUE, | ||
| member_name TEXT NOT NULL, | ||
| member_phone TEXT NOT NULL, | ||
| member_address TEXT NOT NULL | ||
| ); | ||
|
|
||
| CREATE TABLE library_cards ( | ||
| member_id BIGINT PRIMARY KEY REFERENCES members(member_id) ON DELETE CASCADE, | ||
|
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 structural pattern to prevent a second card for the same member. Also proper use of |
||
| card_number TEXT NOT NULL UNIQUE, | ||
| card_issued_on DATE NOT NULL, | ||
| card_expires_on DATE NOT NULL, | ||
| CONSTRAINT library_cards_dates_chk CHECK (card_expires_on > card_issued_on) | ||
| ); | ||
|
|
||
| CREATE TABLE publishers ( | ||
| publisher_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| publisher_name TEXT NOT NULL UNIQUE | ||
| ); | ||
|
|
||
| CREATE TABLE books ( | ||
| book_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| book_isbn TEXT NOT NULL UNIQUE, | ||
| book_title TEXT NOT NULL, | ||
| publisher_id BIGINT NOT NULL REFERENCES publishers(publisher_id) ON DELETE RESTRICT | ||
| ); | ||
|
|
||
| CREATE TABLE authors ( | ||
| author_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| author_name TEXT NOT NULL UNIQUE | ||
| ); | ||
|
|
||
| CREATE TABLE book_authors ( | ||
| book_id BIGINT NOT NULL REFERENCES books(book_id) ON DELETE CASCADE, | ||
| author_id BIGINT NOT NULL REFERENCES authors(author_id) ON DELETE CASCADE, | ||
| PRIMARY KEY (book_id, author_id) | ||
| ); | ||
|
|
||
| CREATE TABLE tags ( | ||
| tag_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| tag_name TEXT NOT NULL UNIQUE | ||
| ); | ||
|
|
||
| CREATE TABLE book_tags ( | ||
| book_id BIGINT NOT NULL REFERENCES books(book_id) ON DELETE CASCADE, | ||
| tag_id BIGINT NOT NULL REFERENCES tags(tag_id) ON DELETE CASCADE, | ||
| PRIMARY KEY (book_id, tag_id) | ||
| ); | ||
|
|
||
| CREATE TABLE copies ( | ||
| copy_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| copy_barcode TEXT NOT NULL UNIQUE, | ||
| book_id BIGINT NOT NULL REFERENCES books(book_id) ON DELETE RESTRICT, | ||
| shelf_code TEXT NOT NULL | ||
| ); | ||
|
|
||
| CREATE TABLE borrowings ( | ||
| borrowing_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| member_id BIGINT NOT NULL REFERENCES members(member_id) ON DELETE RESTRICT, | ||
| copy_id BIGINT NOT NULL REFERENCES copies(copy_id) ON DELETE RESTRICT, | ||
| borrowed_at TIMESTAMP NOT NULL, | ||
|
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.
|
||
| due_date TIMESTAMP NOT NULL, | ||
| returned_at TIMESTAMP NULL, | ||
| fine_eur NUMERIC(10, 2) NOT NULL DEFAULT 0, | ||
| CONSTRAINT borrowings_dates_chk CHECK (due_date > borrowed_at), | ||
| CONSTRAINT borrowings_returned_chk CHECK (returned_at IS NULL OR returned_at >= borrowed_at), | ||
| CONSTRAINT borrowings_fine_chk CHECK (fine_eur >= 0) | ||
| ); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,205 @@ | ||
| package org.hyf; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.sql.Connection; | ||
| import java.sql.DriverManager; | ||
| import java.sql.PreparedStatement; | ||
| import java.sql.ResultSet; | ||
| import java.sql.SQLException; | ||
| import java.sql.Timestamp; | ||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public class Main { | ||
|
|
||
| private static final String URL = "jdbc:postgresql://localhost:5432/library_db"; | ||
| private static final String USER = "postgres"; | ||
| private static final String PASSWORD = "postgres"; | ||
|
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", "postgres");
private static final String PASSWORD = System.getenv("DB_PASSWORD"); |
||
|
|
||
| public static void main(String[] args) { | ||
| System.out.println("You code goes here :) You can overwrite this entire method."); | ||
| try { | ||
| insertData(); | ||
| List<BorrowingRecord> records = fetchData(); | ||
| for (BorrowingRecord r : records) { | ||
| System.out.println(r); | ||
| } | ||
| } catch (SQLException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| } | ||
|
|
||
| public static void insertData() throws SQLException { | ||
| try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) { | ||
| conn.setAutoCommit(false); | ||
| try { | ||
| long tomId = insertMember(conn, "tom@example.com", "Tom", "+31645678901", "Biltstraat 88, Utrecht"); | ||
| long samId = insertMember(conn, "sam@example.com", "Sam", "+31612345678", "Oudegracht 10, Utrecht"); | ||
|
|
||
| insertLibraryCard(conn, tomId, "C-1004", LocalDate.of(2023,11,20), LocalDate.of(2026,11,20)); | ||
| insertLibraryCard(conn, samId, "C-1001", LocalDate.of(2024,1,10), LocalDate.of(2027,1,10)); | ||
|
|
||
| long addisonId = insertPublisher(conn, "Addison-Wesley"); | ||
| long oreillyId = insertPublisher(conn, "O'Reilly"); | ||
|
|
||
| long ejId = insertBook(conn, "9780134685991", "Effective Java", addisonId); | ||
| long hfId = insertBook(conn, "9780596009205", "Head First Java", oreillyId); | ||
|
|
||
| long jb = insertAuthor(conn, "Joshua Bloch"); | ||
| long ks = insertAuthor(conn, "Kathy Sierra"); | ||
|
|
||
| long tagJava = insertTag(conn, "java"); | ||
| long tagBeg = insertTag(conn, "beginners"); | ||
|
|
||
| insertBookAuthor(conn, ejId, jb); | ||
| insertBookAuthor(conn, hfId, ks); | ||
|
|
||
| insertBookTag(conn, ejId, tagJava); | ||
| insertBookTag(conn, hfId, tagBeg); | ||
|
|
||
| long c1 = insertCopy(conn, "BC-0001", ejId, "A-01"); | ||
| long c2 = insertCopy(conn, "BC-0006", hfId, "B-03"); | ||
|
|
||
| insertBorrowing(conn, tomId, c1, LocalDateTime.of(2026,3,9,3,18), LocalDateTime.of(2026,3,30,3,18), LocalDateTime.of(2026,3,28,3,18), new BigDecimal("0.00")); | ||
| insertBorrowing(conn, samId, c2, LocalDateTime.of(2025,12,9,9,8), LocalDateTime.of(2025,12,30,9,8), LocalDateTime.of(2026,1,4,9,8), new BigDecimal("1.25")); | ||
|
|
||
| conn.commit(); | ||
| } catch (SQLException ex) { | ||
| conn.rollback(); | ||
| throw ex; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static List<BorrowingRecord> fetchData() throws SQLException { | ||
| String sql = "SELECT m.member_name, m.member_email, b.book_title, c.copy_barcode, br.borrowed_at, br.due_date, br.returned_at, br.fine_eur " + | ||
| "FROM borrowings br " + | ||
| "JOIN members m ON br.member_id = m.member_id " + | ||
| "JOIN copies c ON br.copy_id = c.copy_id " + | ||
| "JOIN books b ON c.book_id = b.book_id " + | ||
| "ORDER BY br.borrowed_at"; | ||
|
|
||
|
Comment on lines
+81
to
+82
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. You can use a text block for the SQL statements. Also recommend setting your statement to a final constant: """; private static final String BORROW_DATA_SQL = """
SELECT m.member_name, m.member_email, b.book_title, c.copy_barcode, br.borrowed_at, br.due_date, br.returned_at, br.fine_eur
FROM borrowings br
JOIN members m ON br.member_id = m.member_id
JOIN copies c ON br.copy_id = c.copy_id
JOIN books b ON c.book_id = b.book_id
ORDER BY br.borrowed_at
"""; |
||
| try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); | ||
| PreparedStatement preparedStatement = conn.prepareStatement(sql); | ||
| ResultSet resultSet = preparedStatement.executeQuery()) { | ||
|
|
||
| List<BorrowingRecord> list = new ArrayList<>(); | ||
| while (resultSet.next()) { | ||
| Timestamp retTs = resultSet.getTimestamp("returned_at"); | ||
| list.add(new BorrowingRecord( | ||
|
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. Transforming a ResultSet into your domain type is something that will most likely be reused, is a concept and is more a concern of your record import org.hyf.Main;
import java.sql.ResultSet;
record BorrowingRecord(String memberName, String memberEmail, String bookTitle, String copyBarcode,
LocalDateTime borrowedAt, LocalDateTime dueDate, LocalDateTime returnedAt, BigDecimal fine) {
public static BorrowingRecord from(ResultSet fetchedData) throws SQLException {
return new BorrowingRecord(
fetchedData.getString("member_name"),
fetchedData.getString("member_email"),
fetchedData.getString("book_title"),
fetchedData.getString("copy_barcode"),
fetchedData.getObject("borrowed_at", LocalDateTime.class),
fetchedData.getObject("due_date", LocalDateTime.class),
fetchedData.getObject("returned_at", LocalDateTime.class),
fetchedData.getObject("fine_eur", BigDecimal.class)
);
}
}Then you could use it like so: list.add(BorrowingRecord.from(resultSet));While it's possible to move the |
||
| resultSet.getString("member_name"), | ||
| resultSet.getString("member_email"), | ||
| resultSet.getString("book_title"), | ||
| resultSet.getString("copy_barcode"), | ||
| resultSet.getTimestamp("borrowed_at").toLocalDateTime(), | ||
| resultSet.getTimestamp("due_date").toLocalDateTime(), | ||
| retTs == null ? null : retTs.toLocalDateTime(), | ||
|
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. You can use the |
||
| resultSet.getBigDecimal("fine_eur") | ||
| )); | ||
| } | ||
| return list; | ||
| } | ||
| } | ||
|
|
||
| // Helper insert methods using PreparedStatement everywhere | ||
| private static long insertMember(Connection conn, String email, String name, String phone, String address) throws SQLException { | ||
| String sql = "INSERT INTO members (member_email, member_name, member_phone, member_address) VALUES (?, ?, ?, ?) ON CONFLICT (member_email) DO UPDATE SET member_name=EXCLUDED.member_name RETURNING member_id"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setString(1, email); | ||
| preparedStatement.setString(2, name); | ||
| preparedStatement.setString(3, phone); | ||
| preparedStatement.setString(4, address); | ||
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } | ||
|
Comment on lines
+113
to
+114
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. While this is not too many lines of code. It is still repeated a few times. Extracting it out can make refactoring the code later easier and gives you an opportunity to document the code with code by naming the method. Please be aware though, not every piece of code should be extracted as it can reduce readability. Only extract when you see a clear, reusable concept (like "execute an insert that returns an ID"). If extracting would split business logic across multiple methods and make the flow harder to follow, it’s better to keep it together. In this case, the extraction is justified because:
|
||
| } | ||
|
|
||
| private static void insertLibraryCard(Connection conn, long memberId, String cardNumber, LocalDate issued, LocalDate expires) throws SQLException { | ||
| String sql = "INSERT INTO library_cards (member_id, card_number, card_issued_on, card_expires_on) VALUES (?, ?, ?, ?) ON CONFLICT (member_id) DO UPDATE SET card_number=EXCLUDED.card_number"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setLong(1, memberId); | ||
| preparedStatement.setString(2, cardNumber); | ||
| preparedStatement.setObject(3, issued); | ||
| preparedStatement.setObject(4, expires); | ||
| preparedStatement.executeUpdate(); | ||
| } | ||
| } | ||
|
|
||
| private static long insertPublisher(Connection conn, String name) throws SQLException { | ||
| String sql = "INSERT INTO publishers (publisher_name) VALUES (?) ON CONFLICT (publisher_name) DO UPDATE SET publisher_name=EXCLUDED.publisher_name RETURNING publisher_id"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setString(1, name); | ||
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } | ||
| } | ||
|
|
||
| private static long insertBook(Connection conn, String isbn, String title, long publisherId) throws SQLException { | ||
| String sql = "INSERT INTO books (book_isbn, book_title, publisher_id) VALUES (?, ?, ?) ON CONFLICT (book_isbn) DO UPDATE SET book_title=EXCLUDED.book_title RETURNING book_id"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setString(1, isbn); | ||
| preparedStatement.setString(2, title); | ||
| preparedStatement.setLong(3, publisherId); | ||
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } | ||
| } | ||
|
|
||
| private static long insertAuthor(Connection conn, String name) throws SQLException { | ||
| String sql = "INSERT INTO authors (author_name) VALUES (?) ON CONFLICT (author_name) DO UPDATE SET author_name=EXCLUDED.author_name RETURNING author_id"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setString(1, name); | ||
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } | ||
| } | ||
|
|
||
| private static long insertTag(Connection conn, String name) throws SQLException { | ||
| String sql = "INSERT INTO tags (tag_name) VALUES (?) ON CONFLICT (tag_name) DO UPDATE SET tag_name=EXCLUDED.tag_name RETURNING tag_id"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setString(1, name); | ||
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } | ||
| } | ||
|
|
||
| private static void insertBookAuthor(Connection conn, long bookId, long authorId) throws SQLException { | ||
| String sql = "INSERT INTO book_authors (book_id, author_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setLong(1, bookId); | ||
| preparedStatement.setLong(2, authorId); | ||
| preparedStatement.executeUpdate(); | ||
| } | ||
| } | ||
|
|
||
| private static void insertBookTag(Connection conn, long bookId, long tagId) throws SQLException { | ||
| String sql = "INSERT INTO book_tags (book_id, tag_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setLong(1, bookId); | ||
| preparedStatement.setLong(2, tagId); | ||
| preparedStatement.executeUpdate(); | ||
| } | ||
| } | ||
|
|
||
| private static long insertCopy(Connection conn, String barcode, long bookId, String shelfCode) throws SQLException { | ||
| String sql = "INSERT INTO copies (copy_barcode, book_id, shelf_code) VALUES (?, ?, ?) ON CONFLICT (copy_barcode) DO UPDATE SET book_id=EXCLUDED.book_id RETURNING copy_id"; | ||
| try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { | ||
| preparedStatement.setString(1, barcode); | ||
| preparedStatement.setLong(2, bookId); | ||
| preparedStatement.setString(3, shelfCode); | ||
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } | ||
| } | ||
|
|
||
| private static void insertBorrowing(Connection conn, long memberId, long copyId, LocalDateTime borrowedAt, LocalDateTime dueDate, LocalDateTime returnedAt, BigDecimal fine) throws SQLException { | ||
| String sql = "INSERT INTO borrowings (member_id, copy_id, borrowed_at, due_date, returned_at, fine_eur) VALUES (?, ?, ?, ?, ?, ?)"; | ||
| try (PreparedStatement ps = conn.prepareStatement(sql)) { | ||
| ps.setLong(1, memberId); | ||
| ps.setLong(2, copyId); | ||
| ps.setTimestamp(3, Timestamp.valueOf(borrowedAt)); | ||
| ps.setTimestamp(4, Timestamp.valueOf(dueDate)); | ||
| ps.setTimestamp(5, returnedAt == null ? null : Timestamp.valueOf(returnedAt)); | ||
| ps.setBigDecimal(6, fine); | ||
| ps.executeUpdate(); | ||
| } | ||
| } | ||
|
|
||
| public static record BorrowingRecord(String memberName, String memberEmail, String bookTitle, String copyBarcode, LocalDateTime borrowedAt, LocalDateTime dueDate, LocalDateTime returnedAt, BigDecimal fine) {} | ||
|
|
||
| } | ||
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.
The powershell script doesn't work the first time it's run for me.
On the second run it does. Which is because of a truthy value coming from
docker exec library_db pg_isready -U postgres -d postgres 2>$null2>$nullonly silences the stderr channel but pg_isready prints status text to the stdout (1) channel. Even if the container hasn't started yet (Eg: rejecting connections). So this evaluates to true.You can use the below check instead to determine if your database is ready. $LASTEXITCODE retrieves the last process exist number from powershell,
docker execcommand returns non-zero until the db is accepting connections.