How Spring-JPA sucks big time

Posted by {"name"=>"Palash Ray", "email"=>"paawak@gmail.com", "url"=>"https://www.linkedin.com/in/palash-ray/"} on November 04, 2017 · 5 mins read

Ok, so all I am trying to do is save a new Entity into the DB and then retrieving it using its ID.

Saving my Entity

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 Set genres;

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.

Fetching the Entity I just now saved

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.

Implementation details with Spring JPA

Repository Layer

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 JpaRepository bookRepo;
	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 Layer

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

Implementing with standard JPA

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.

Sources

An working example of this can be found here:
https://github.com/paawak/blog/tree/master/code/one-to-many-example-2