Monerh A#4
Conversation
cometbroom
left a comment
There was a problem hiding this comment.
The schema is normalized cleanly, all required relationship types are present, and the assignment satisfies the JDBC requirements with prepared statements and transaction handling.
Did add a few comments to look into. Optional to implement them on this PR ofcourse.
| "ORDER BY br.borrowed_at"; | ||
|
|
There was a problem hiding this comment.
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
""";| 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.
TIMESTAMP stores the time given to it without accounting for timezone which can cause mismatch in timing if you have servers running in different timezones or you have libraries in different timezones.
The type TIMESTAMPTZ converts the value to UTC and standardizes how time is stored on your database. Then any library that views this can decide to show UTC, local time or both times.
| try (ResultSet resultSet = preparedStatement.executeQuery()) { resultSet.next(); return resultSet.getLong(1); } | ||
| } |
There was a problem hiding this comment.
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:
- The pattern appears multiple times
- The concept ("run this SQL and give me back the generated ID") is cohesive
| 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.
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 BorrowingRecord.
You can add a static from method in your record that parses from a ResultSet instead.
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 while block also in the record's static method, that could harm readability and reusability. Because then the record is worrying about arranging the data and is less reusable for different fetch scenarios which need different arrangment including having just a single record.
| while (-not (docker exec library_db pg_isready -U postgres -d postgres 2>$null)) { Start-Sleep -Milliseconds 100 } | ||
| docker exec library_db psql -U postgres -d library_db -v ON_ERROR_STOP=1 -f /schema/schema.sql |
There was a problem hiding this comment.
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>$null
2>$null only 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 exec command returns non-zero until the db is accepting connections.
do {
Start-Sleep -Milliseconds 250
docker exec library_db pg_isready -h localhost -U postgres -d library_db 2> $null
$ready = $LASTEXITCODE -eq 0
} until ($ready)|
|
||
| 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.
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");| 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.
You can use the getObject() method with LocalDateTime.class type (See my other comment on fetchData). Then no null-check is needed.
| ); | ||
|
|
||
| CREATE TABLE library_cards ( | ||
| member_id BIGINT PRIMARY KEY REFERENCES members(member_id) ON DELETE CASCADE, |
There was a problem hiding this comment.
Nice structural pattern to prevent a second card for the same member. Also proper use of ON DELETE CASCADE. Well done.
This PR adds the library schema, a Java JDBC demo, and run instructions.
Schema
Java demo
insertData()(inserts sample rows — ≥2 where applicable) andfetchData()(retrieves borrowings using JOINs).PreparedStatementfor all DB access and explicit transaction handling (commit / rollback).Run instructions