Ok, so all I am trying to do is save a new Entity into the DB and then retrieving it using its ID.
The entity Book contains an Author, some Chapters and some Genres as shown below:
@Entity @Table(name = "BOOK") public class Book implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bookIdGenerator") @SequenceGenerator(name = "bookIdGenerator", sequenceName = "SEQ_BOOK_ID") @Column(name = "id") private Long id; @Column(name = "title") private String title; @OneToOne(fetch = FetchType.EAGER) @JoinColumn(name = "author_id") private Author author; @OneToOne(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinColumn(name = "main_chapter_id") private Chapter mainChapter; @OneToMany(fetch = FetchType.EAGER) @JoinTable(name = "BOOK_GENRE", joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "genre_id", referencedColumnName = "id")) private Setgenres;
While saving, the assumption is that the Author and the Genres are already present in the DB. While saving a new Book, I pass all the attributes of the Book and the Chapters. However, since the Author and the Genres are already present in the DB, I am passing their IDs only and not all of their attributes; pretty much like a Foreign Key. I am illustrating this with the below JSON:
{ "title": "The Bazilian Cat", "author": { "id": 10 }, "mainChapter": { "title": "main CHAPTER for brazilian cat", "plotSummary": null, "contents": { "id": 30 }, "subChapters": [ { "title": "aaaaa", "plotSummary": null, "contents": { "id": 1 } } ] }, "genres": [ { "id": 3 }, { "id": 2 } ] }
Note that for author and genres, we are only passing their IDs and no other attributes.
Now, after saving the new Book, when I fetch, my expectation is that Spring-JPA fetches the entire object graph faithfully, without missing any attributes. In reality, it never even bothers to hit the DB with a query, but returns the Book from the Session Cache itself. So, when I fetch my newly saved Book, only the IDs are fetched for Author and Genre, and no other attribute.
The interface BookDao defines the contract.
public interface BookDao { Book getBook(Long bookId); Long saveNewBook(Book book); }
The implementation looks like:
public class JpaBasedBookDao implements BookDao { private static final Logger LOGGER = LoggerFactory.getLogger(JpaBasedBookDao.class); private final JpaRepositorybookRepo; public JpaBasedBookDao(JpaRepository bookRepo) { this.bookRepo = bookRepo; } @Transactional(readOnly = true) @Override public Book getBook(Long bookId) { LOGGER.info("retrieving book with id: {}", bookId); return bookRepo.findOne(bookId); } @Transactional @Override public Long saveNewBook(Book book) { LOGGER.info("saving book: {}", book); return bookRepo.saveAndFlush(book).getId(); } }
@Service public class BookServiceImpl implements BookService { private final BookDao bookRepo; @Autowired public BookServiceImpl(BookDao bookRepo) { this.bookRepo = bookRepo; } @Override public Book getBook(Long bookId) { return bookRepo.getBook(bookId); } @Override public Book saveOrUpdate(Book book) { Long id = bookRepo.saveNewBook(book); return bookRepo.getBook(id); } }
Note that the Transactions are started in the Repository layer. So, when I do a saveOrUpdate() in my service, there are 2 transactions that are happening. However, despite that, the Book is returned from the Session Cache and I get an incomplete object graph. Spring JPA does not give me much leverage to clear or evict or refresh the Session Cache after the insert happens.
This can be handled better with the plain vanilla JPA. This is how the Repository looks like:
public class EntityManagerBasedBookDao implements BookDao { private static final Logger LOGGER = LoggerFactory.getLogger(EntityManagerBasedBookDao.class); private final EntityManager entityManager; public EntityManagerBasedBookDao(EntityManager entityManager) { this.entityManager = entityManager; } @Transactional(readOnly = true) @Override public Book getBook(Long bookId) { LOGGER.info("retrieving book with id: {}", bookId); Book book = entityManager.find(Book.class, bookId); entityManager.refresh(book); return book; } @Transactional @Override public Long saveNewBook(Book book) { LOGGER.info("saving book: {}", book); entityManager.persist(book); return book.getId(); } }
The below line is particularly important in the method getBook(Long bookId):
entityManager.refresh(book);
Without the above line, we will still get an incomplete object graph.
An working example of this can be found here:
https://github.com/paawak/blog/tree/master/code/one-to-many-example-2