Skip to content

Features/loan#10

Merged
eafalkens merged 8 commits into
mainfrom
features/loan
Jan 12, 2026
Merged

Features/loan#10
eafalkens merged 8 commits into
mainfrom
features/loan

Conversation

@eafalkens

@eafalkens eafalkens commented Jan 12, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

New Features

  • Implemented complete book loan system enabling users to borrow books and track returns
  • Added book availability status checking to display current loan information
  • Loan history now records loan dates and return dates for all borrowing activities

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes refactor the loan management model from ID-based to object-based relationships. Book gains accessors for its Loan relationship. Loan replaces primitive ID fields with User and Book object references through JPA annotations. A new LoanServices class provides business logic for loan operations.

Changes

Cohort / File(s) Summary
Entity Relationship Refactoring
src/main/java/org/example/Book.java, src/main/java/org/example/Loan.java
Book: Added getLoan() and setLoan(Loan) accessors. Loan: Replaced ID-based accessors (getBookId, setBookId, getUserId, setUserId) with object-based accessors (getBook, setBook, getUser, setUser). Introduced @ManyToOne User field and @OneToOne(mappedBy = "loan") Book field.
New Service Layer
src/main/java/org/example/LoanServices.java
New public service class providing loan management operations: isBookLoaned(Long) checks if a book has an active loan; loanBook(Long, Long) creates a new loan if the book is available; returnBook(User, Book) marks a loan as returned and updates related entities. Uses EntityManager for persistence.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant LS as LoanServices
    participant EM as EntityManager
    participant DB as Database
    participant B as Book
    participant U as User
    participant L as Loan

    App->>LS: loanBook(bookId, userId)
    LS->>LS: isBookLoaned(bookId)
    LS->>EM: Query Loan entities
    EM->>DB: SELECT loan WHERE bookId=? AND returnDate=null
    DB-->>EM: Result
    alt Book already loaned
        LS-->>App: false
    else Book available
        LS->>EM: Find User by userId
        EM->>DB: SELECT user WHERE id=userId
        DB-->>EM: User entity
        LS->>EM: Find Book by bookId
        EM->>DB: SELECT book WHERE id=bookId
        DB-->>EM: Book entity
        LS->>L: Create Loan(loanDate=now, returnDate=null)
        L->>B: Set book reference
        B->>L: Set loan reference
        LS->>EM: Persist Loan
        EM->>DB: INSERT into Loan
        DB-->>EM: Success
        LS-->>App: true
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Features/entity #6: Updates Book and Loan entities to expose and use object associations (Book.getLoan/setLoan and Loan.book/Loan.user), directly addressing the same Book–Loan relationship refactored in this PR.

Poem

A rabbit hops through code so bright, 🐰
Where loans and books now dance with light,
No more IDs to confuse the way,
Objects linked, hip-hip-hooray!
Relations bloom in JPA's delight. 🌿

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Features/loan' is vague and does not clearly convey what the pull request accomplishes. It appears to be a branch name rather than a descriptive title. Replace with a clear, descriptive title summarizing the main change, such as 'Refactor loan management with object relationships instead of IDs' or 'Implement loan service with Book-User-Loan associations'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/main/java/org/example/LoanServices.java:
- Around line 53-75: The returnBook method currently starts a transaction with
em.getTransaction().begin() but does not rollback on exception and also uses
entity parameters unlike loanBook(Long, Long); update returnBook(User, Book) to
wrap the transaction in a try-catch-finally (or use a transaction helper) so
that on any exception you call em.getTransaction().rollback() before rethrowing
or returning false, and ensure the transaction is committed only on success;
additionally align the API by either adding a returnBook(Long userId, Long
bookId) overload (mirroring loanBook) or change loanBook to accept entities so
both methods use the same parameter style and lookup logic (refer to returnBook,
loanBook, em, Loan, setReturnDate).
- Around line 33-50: In loanBook(Long bookId, Long userId) ensure you 1)
validate em.find results (User user and Book book) and return false or throw if
either is null; 2) start and commit/rollback a transaction around the
persistence (use EntityTransaction tx =
em.getTransaction()/tx.begin()/tx.commit() and tx.rollback() on exceptions); and
3) update the owning side before persisting by calling book.setLoan(loan) (in
addition to loan.setBook(book)) so the @JoinColumn is saved; also wrap
persistence in try/catch to rollback the transaction on error.
🧹 Nitpick comments (1)
src/main/java/org/example/LoanServices.java (1)

16-30: Simplify the boolean return.

The conditional block can be reduced to a single expression.

♻️ Suggested simplification
-        if (loans.isEmpty()) {
-            return false;
-        } else {
-            return true;
-        }
+        return !loans.isEmpty();
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e028d8a and 74045ae.

📒 Files selected for processing (3)
  • src/main/java/org/example/Book.java
  • src/main/java/org/example/Loan.java
  • src/main/java/org/example/LoanServices.java
🔇 Additional comments (3)
src/main/java/org/example/Book.java (1)

67-85: LGTM with a note on field ordering.

The accessors are correctly implemented. Note that Book is the owning side of the @OneToOne relationship (it has @JoinColumn), so book.setLoan(loan) must be called to persist the association—setting only loan.setBook(book) won't update the foreign key.

Consider moving the loan field declaration (lines 84-85) to be grouped with other fields at the top of the class for consistency.

src/main/java/org/example/Loan.java (2)

55-59: Verify the relationship ownership design.

The @OneToOne(mappedBy = "loan") makes Loan the inverse side of the Book-Loan relationship. This means:

  • Book owns the foreign key (loaned_book column)
  • Updates to loan.book alone won't persist—book.loan must also be set

This is semantically unusual (typically a Loan would "own" its Book reference). If intentional, ensure all service code sets the owning side (book.setLoan(loan)). Consider also moving field declarations above accessors for conventional ordering.


19-33: Accessors look correct.

The object-based getters/setters properly replace the previous ID-based accessors.

Comment on lines +33 to +50
public boolean loanBook(Long bookId, Long userId) {

if (isBookLoaned(bookId)) {
return false;
}

User user = em.find(User.class, userId);
Book book = em.find(Book.class, bookId);

Loan loan = new Loan();

loan.setUser(user);
loan.setBook(book);
loan.setLoanDate(ZonedDateTime.now());
loan.setReturnDate(null);
em.persist(loan);
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Owning-side not updated, missing transaction and null checks.

Several issues in this method:

  1. Relationship not persisted: Book is the owning side (@JoinColumn), so you must call book.setLoan(loan) for the FK to be saved—loan.setBook(book) alone won't persist the association.
  2. No transaction: em.persist() requires an active transaction.
  3. No null checks: em.find() returns null if the entity doesn't exist—dereferencing will cause NPE.
🐛 Proposed fix
     public boolean loanBook(Long bookId, Long userId) {

         if (isBookLoaned(bookId)) {
             return false;
         }

         User user = em.find(User.class, userId);
         Book book = em.find(Book.class, bookId);

+        if (user == null || book == null) {
+            return false;
+        }
+
+        em.getTransaction().begin();
+
         Loan loan = new Loan();

         loan.setUser(user);
         loan.setBook(book);
         loan.setLoanDate(ZonedDateTime.now());
-        loan.setReturnDate(null);
+        book.setLoan(loan);  // Set owning side to persist the FK
+
         em.persist(loan);
+        em.getTransaction().commit();
         return true;
     }
🤖 Prompt for AI Agents
In @src/main/java/org/example/LoanServices.java around lines 33 - 50, In
loanBook(Long bookId, Long userId) ensure you 1) validate em.find results (User
user and Book book) and return false or throw if either is null; 2) start and
commit/rollback a transaction around the persistence (use EntityTransaction tx =
em.getTransaction()/tx.begin()/tx.commit() and tx.rollback() on exceptions); and
3) update the owning side before persisting by calling book.setLoan(loan) (in
addition to loan.setBook(book)) so the @JoinColumn is saved; also wrap
persistence in try/catch to rollback the transaction on error.

Comment on lines +53 to +75
public boolean returnBook(User user, Book book) {

Loan loan = em.createQuery(
"SELECT l FROM Loan l WHERE l.user = :user AND l.book = :book AND l.returnDate IS NULL",
Loan.class
)
.setParameter("user", user)
.setParameter("book", book)
.getResultStream()
.findFirst()
.orElse(null);

if (loan == null) {
return false;
}

em.getTransaction().begin();

loan.setReturnDate(ZonedDateTime.now());
book.setLoan(null);
em.getTransaction().commit();
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing rollback handling and inconsistent API design.

  1. No rollback on failure: If an exception occurs between begin() and commit(), the transaction is left dangling. Wrap in try-catch or use try-with-resources with a transaction wrapper.
  2. Inconsistent API: loanBook(Long, Long) takes IDs while returnBook(User, Book) takes entities. Consider aligning the signatures for consistency.
🔧 Suggested fix with rollback handling
     public boolean returnBook(User user, Book book) {

         Loan loan = em.createQuery(
             "SELECT l FROM Loan l WHERE l.user = :user AND l.book = :book AND l.returnDate IS NULL",
             Loan.class
         )
             .setParameter("user", user)
             .setParameter("book", book)
             .getResultStream()
             .findFirst()
             .orElse(null);

         if (loan == null) {
             return false;
         }

-        em.getTransaction().begin();
-
-        loan.setReturnDate(ZonedDateTime.now());
-        book.setLoan(null);
-        em.getTransaction().commit();
-        return true;
+        try {
+            em.getTransaction().begin();
+            loan.setReturnDate(ZonedDateTime.now());
+            book.setLoan(null);
+            em.getTransaction().commit();
+            return true;
+        } catch (Exception e) {
+            if (em.getTransaction().isActive()) {
+                em.getTransaction().rollback();
+            }
+            throw e;
+        }
     }
🤖 Prompt for AI Agents
In @src/main/java/org/example/LoanServices.java around lines 53 - 75, The
returnBook method currently starts a transaction with
em.getTransaction().begin() but does not rollback on exception and also uses
entity parameters unlike loanBook(Long, Long); update returnBook(User, Book) to
wrap the transaction in a try-catch-finally (or use a transaction helper) so
that on any exception you call em.getTransaction().rollback() before rethrowing
or returning false, and ensure the transaction is committed only on success;
additionally align the API by either adding a returnBook(Long userId, Long
bookId) overload (mirroring loanBook) or change loanBook to accept entities so
both methods use the same parameter style and lookup logic (refer to returnBook,
loanBook, em, Loan, setReturnDate).

@eafalkens
eafalkens merged commit 2419914 into main Jan 12, 2026
2 checks passed
This was referenced Jan 13, 2026
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.

1 participant