I have two classes: Book and Member. In the Member class there are two HashSet which store Books: borrowedBooks
and returnedBooks
.
When I remove one Book from borrowedBooks
and put it into returnedBook
, Hibernate will do that without a problem for all except for the last element in borrowedBooks
. However, if i remove the last element in borrowedBooks
Hibernate also removes all of the Books in returnedBook
. So, at the end of scenario there are no Books in borrowedBooks
, but there are also no Books in returnedBooks
.
For example:
1) borrowedBooks: a, b, c
1) returnedBooks:
---
2) borrowedBooks: a, b
2) returnedBooks: c
---
3) borrowedBooks: a
3) returnedBooks: b, c
---
4) borrowedBooks: -
4) returnedBooks: -
That's really not understandable! Why does this happen? Thanks a lot for your help! Here are my classes:
@Entity
public class Book {
@Id
@TableGenerator(...)
@GeneratedValue(generator = "Book_Barcode")
private long barcode;
private long isbn=0;
private String bookTitle="";
// some other fields
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (barcode ^ (barcode >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Book other = (Book) obj;
if (barcode != other.barcode)
return false;
return true;
}
}
@Entity
public class Member {
@Id
@TableGenerator(...)
@GeneratedValue(generator = "Member_Id")
private int id;
@OneToMany(fetch = FetchType.EAGER) //please ignore FetchType!
private Set<Book> returnedBooks = new HashSet<>();
@OneToMany(fetch = FetchType.EAGER)
private Set<Book> borrowedBooks = new HashSet<>();
@OneToMany(fetch = FetchType.EAGER)
private Set<Book> renewedBooks = new HashSet<>();
}
@Repository
public class MemberDAOImpl implements MemberDAO {
@Autowired
private SessionFactory sessionFactory;
@Override
@Transactional
public void borrowBook(Member member, Book book) {
if (book.getStatus().equals("Available") &&
book.getAvailableAmount() > 0) {
Session currentSession = sessionFactory.getCurrentSession();
book.setBorrowedDate(new Date());
book.setAvailableAmount(book.getAvailableAmount() - 1);
book.setStatus("Borrowed");
Member newMember = currentSession.find(Member.class, member.getId());
newMember.getBorrowedBooks().add(book);
}
}
@Override
@Transactional
public void returnBook(Member member, Book book) {
if (book.getStatus().equals("Borrowed")) {
Session currentSession = sessionFactory.getCurrentSession();
Member newMember = currentSession.find(Member.class, member.getId());
newMember.getReturnedBooks().add(book);
newMember.getBorrowedBooks().remove(book);
book.setBorrowedDate(null);
book.setAvailableAmount(book.getAvailableAmount() + 1);
book.setStatus("Available");
book.setRenewedTimes(0);
}
}
}
@Controller
@RequestMapping("/member")
public class MemberController {
static Member member;
@Autowired
private MemberService memberService;
@Autowired
private BookService bookService;
@GetMapping("/bookBorrow")
public String bookBorrow(@RequestParam("barcode") long barcode, Model model) {
Book book = bookService.searchBookByBarcode(barcode);
memberService.borrowBook(member, book);
model.addAttribute("booksList", member.getBorrowedBooks());
model.addAttribute("member", member);
return "member-specific-home-page";
}
@GetMapping("/bookReturn")
public String bookReturn(@RequestParam("barcode") long barcode, Model model) {
Book book = bookService.searchBookByBarcode(barcode);
memberService.returnBook(member, book);
model.addAttribute("booksList", member.getReturnedBooks());
model.addAttribute("member", member);
return "member-specific-home-page";
}
}
So, I believe there is no problem in public void borrowBook(...)
. Is there something wrong in public void returnBook(...)
? I spent a lot of time but i could not find a way... Thanks in advance!
================================ There is something wrong with Hibernate! For example: if i have 3 books in borrowedBooks set and than if i try to return them: so remove from borrowedBooks and put in returnedBook.
FIRST RETURN:
Hibernate: update Book set amount=?, availableAmount=?, bookTitle=?, borrowedDate=?, description=?, editedDate=?, edition=?, isbn=?, issuedDate=?, language=?, page=?, price=?, publisher=?, registrationDate=?, renewedDate=?, renewedTimes=?, status=? where barcode=?
Hibernate: insert into Member_Book (Member_id, returnedBooks_barcode) values (?, ?)
Hibernate: delete from Member_Book where Member_id=? and borrowedBooks_barcode=?
SECOND RETURN:
Hibernate: update Book set amount=?, availableAmount=?, bookTitle=?, borrowedDate=?, description=?, editedDate=?, edition=?, isbn=?, issuedDate=?, language=?, page=?, price=?, publisher=?, registrationDate=?, renewedDate=?, renewedTimes=?, status=? where barcode=?
Hibernate: insert into Member_Book (Member_id, returnedBooks_barcode) values (?, ?)
Hibernate: delete from Member_Book where Member_id=? and borrowedBooks_barcode=?
THIRD RETURN:
Hibernate: update Book set amount=?, availableAmount=?, bookTitle=?, borrowedDate=?, description=?, editedDate=?, edition=?, isbn=?, issuedDate=?, language=?, page=?, price=?, publisher=?, registrationDate=?, renewedDate=?, renewedTimes=?, status=? where barcode=?
Hibernate: insert into Member_Book (Member_id, returnedBooks_barcode) values (?, ?)
Hibernate: delete from Member_Book where Member_id=?
PLEASE LOOK AT THE "LAST" DELETE OPERATION: delete from Member_Book where Member_id=? WHY JUST "where Member_id=?" ???
Not sure this is the right answer, but two things stand of in your code :
Session / Transacitonal boundaries
Your controler is not transactionnal (which is OK).
@GetMapping("/bookBorrow")
public String bookBorrow(@RequestParam("barcode") long barcode, Model model) {
Book book = bookService.searchBookByBarcode(barcode);
memberService.returnBook(member, book);
//...
Your memberService.returnBook()
is :
@Transactional
public void returnBook(Member member, Book book) {
So we can infer that for a single HTTP request (call to your controler), there is one hibernate session opened for bookService.searchBook...
, and another one opened in memberService.returnBook
.
When you have two hibernate sessions sharing the same entities, "funny" things happen. You should not use the book
instance got from the bookService
inside the memeberService
, at least not without reattaching it.
(Well actually my advice would be to have your whole controler dispatch to a single transaction and hibernate session, not two, and not have to worry about stuff like that).
What does "funny" mean ? The main issue is that hibernate guarantees you that, in a single session, any handle it gives you to a persistent object (book, member), is the same, as in : they are ==
. If you are in a single session, then bookService.load(id) == bookService.load(id)
. This is not the case in your current controler implementation, so the objects may or may not be the same.
Which is kind of probelmatic, because... issue 2
HashCode / Equals design
Your hashCode and equals are not aligned with a reasonnable intent. Basically your book hashCode
is a hash of the barCode. And your equals is an ==
comparison on the barCode. I bet you meant .equals()
, but you wrote "==".
So unless two books have the same barcode String instance (highly unlikely), they are never equal.
So what happens ?
Well, you fetch a book in a hibernate session, you get back an instance that I will call Book@1, with a barcode "123" which is a String instance that I will call String@1.
Then you close the hibernate session, and you enter another one.
In this session, you load a Member, Member@1, which has a set of books, that hibernates load, but you are in a new Session. So hibernate loads a new book instance each time, and you end up with Book instance Book@2, with barcode "123" which is String@2 (the strings hold the same chars, they are .equals
, but they are not ==
).
So you remove Book@1 from the Member@1's book set. What does the implementation do ? It looks if Book@2 and Book@1 are the same. .hashcode()
wise, they are. .equals()
wise ? They are not. The @Book1 is not @Book2 and is not equal to it, so it is not removed.
Another consequence, is that whatever you do to Book@1 is not tracked by hibernate, because the session that created Book@1 is closed. So it actually becomes a gray area : what if you add Book@1 to Member@1. How could hibernate know that it actually not is a new book that you just created ? Should it know it at all ?
Now what ?
Fix your equals. It's good that you have a "natural key" for books, and that you use it, but you probably used ==
where you should have used .equals()
Have proper transaction boundaries. Hibernate will work a lot better if you do not spend time creating situations where you have different instances of the same entity.
User proper naming : what you call MemberDAO is not a DAO. A DAO accesses and stores objects in accordance to query patterns. Here, your MemberDAO manipulates different entities in accordance to business logic (e.g. if I give a book back, increment an availability counter). This is not a DAO's job, it is a service's job. Proper naming will usually help you set proper transaction boundaries, which will help proper session demarcation. (E.g. it is usually suspicious to have @Transactionnal on an DAO implementation, unless you "cut corners". It happens, if you write a really simple CRUD service and you want to save time, but when business logic sneaks in, this is a code smell to have a @Transactionnal DAO).
And finally, use a step by step debugger. Hibernate does not usually send SQL that does not match the state of you object's instances.