Why does Hibernate remove all elements in a set if i remove all elements of another set in the same class?

Rizgar :

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=?" ???

GPI :

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.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=109980&siteId=1