Up-coming Platform Issue: Hibernate and JPA

It seems that we’re going to have to think about how we want to address some up-coming changes in Hibernate. In particular, Hibernate 6 drops a number of legacy Hibernate features that have been deprecated for some time. The most painful of these is the removal of the Hibernate Criteria API in favour of the JPA Criteria API. While we don’t always make heavy use of the Hibernate Criteria API (unless you are, say, the idiot who decided it was a great idea to base the FHIR module’s database access around), it does show up in a number of places. For example, I noticed this was a bit of a potential issue while building out tests for the reporting module on core 2.4, which uses a version of Hibernate that supports the Criteria API but adds many logging messages noting that it’s deprecated.

The JPA Criteria API offers a number of advantages, from allowing multiple joins against the same table (which is useful when writing queries against an EAV store like the Obs table) to completely type-safe queries using a generated metamodel (which is useful for compile-time checking that queries actually do what the developer thinks they do). However, using it requires using a JPA EntityManager rather than a raw Hibernate Session (this is less true in more up-to-date versions of Hibernate 5 where the Session is the EntityManager implementation; unfortunately, this was not the case for Hibernate ORM < 5.2, so roughly basically, for versions of the OpenMRS platform before 2.4).

For an issue that affects the platform, this is actually less of a problem for openmrs-core than it is for the module eco-system. I.e., we can relatively easily migrate core away from the Hibernate Criteria API, but any modules that make use of features sunset in Hibernate 6 likely will need to be updated to adapt to it.

Of course, we can always pin things to the Hibernate 5.x series, and I don’t expect Hibernate 6 to be out anytime soon, but I think it’s worth considering whether we an make this eventual transition easier, e.g., by migrating things towards using JPA .

3 Likes

My POV would be break it now & get everything ready for Hibernate 6 for Platform 2.5 and reduce the future pain we shall definitely face

Thanks so much for this thoughtful post, @ibacher. We could consider a combination of:

  • Incremental approaches in Platform 2.6+
  • One or more hackathon(s) to knock out changes that are relatively straightforward but affecting many areas of the code.
  • Leveraging GSoC 2022 by creating intro tickets for applicants to knock out and/or creating GSoC project(s) to move it forward.
  • Target Platform 3.0 as a transition toward scale, where we let go of hot module loading and simplify the module framework to focus on a more robust and scalable build-time architecture that embraces changes like what you’ve described here. @raff has been proposing such improvements that would make development more efficient, make the platform more scalable, and be a perfect opportunity to adopt changes like platform-wide JPA.
1 Like

This is probably a discussion for the 5-star generals…:slight_smile: Nevertheless, I will provide my thoughts. The JPA Criteria API has its advantages as clearly stated by @ibacher. However, Spring Data JPA ticks most of the boxes and provides even more.

1. Executing Basic JPQL Queries

If the query isn’t too complicated and doesn’t use more than 2 bind parameters, the derived query feature can be used.

public interface AuthorRepository extends JpaRepository<Author, Long> {
    List<Author> findByFirstName(String firstName);
}

2. What About Complicated Queries?

Spring Data JPA’s @Query annotation gives you full flexibility over the executed statement, and your method name doesn’t need to follow any conventions. The only thing you need to do is to define a method in your repository interface, annotate it with @Query, and provide the statement that you want to execute.

public interface AuthorRepository extends JpaRepository<Author, Long> {        
    @Query("FROM Author WHERE firstName = ?1 ORDER BY lastName ASC")
    List<Author> findByFirstNameOrderByLastname(String firstName);
}

We can also compose Repositories Using Multiple Fragments;

public interface CustomItemTypeRepository {
    void deleteCustom(ItemType entity);
    void findThenDelete(Long id);
}

public interface CustomItemRepository {
    Item findItemById(Long id);
    void deleteCustom(Item entity);
    void findThenDelete(Long id);
}

Of course, we’d need to write their implementations. But instead of plugging these custom repositories – with related functionalities – in their own JPA repositories, we can extend the functionality of a single JPA repository:

public interface ItemTypeRepository 
  extends JpaRepository<ItemType, Long>, CustomItemTypeRepository, CustomItemRepository {}

3. Using DTO projections.

Spring Data JPA only requires defining an interface to use as a return type rather than implementing a DTO class.

4. Paginate Query Results.

Spring Data JPA’s Pageable interface makes pagination a little bit easier.

public interface BookRepository extends JpaRepository<Book, Long> {
    Page<Book> findAll(Pageable pageable); 
}

5. Using EntityGraphs

An EntityGraph provides an easy and reusable way to initialize required entity associations within the query. Instead of executing an additional query for each entity association, which is known as the n+1 select issue,

So Why Do We Really Need Spring Data JPA?

Well, JPA’s Criteria API suffers from a similar issue as the Hibernate Criteria API. You define the filter operations on the Criteria that represents the JOIN clause. But it’s very different from the actual SQL statement that Hibernate has to generate. At the same time, the verbosity of the Criteria API reduces the readability of your code. Using this (With Hibernate) alone means more boilerplate code.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Author> cq = cb.createQuery(Author.class);
Root<Author> root = cq.from(Author.class);
SetJoin<Author, Book> books = root.join(Author_.books);
 
ParameterExpression<String> paramTitle = cb.parameter(String.class);
cq.where(cb.like(books.get(Book_.title), paramTitle));
 
TypedQuery<Author> q = em.createQuery(cq);
q.setParameter(paramTitle, "%Hibernate%");
List<Author> authors = q.getResultList();

Conclusion

We can use Spring Data JPA as our data access abstraction layer to leverage some of its advantages. Spring Data JPA uses Hibernate under the hood and it’s compatible with the *Criteria Query API if the goal is to offer a programmatic way to create typed queries, which helps us avoid syntax errors. (especially for complex queries).

@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Book> cq = cb.createQuery(Book.class);
    Root<Book> book = cq.from(Book.class);
    List<Predicate> predicates = new ArrayList<>();
   
    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));
    return em.createQuery(cq).getResultList();
}
1 Like

Thanks @ibacher , @miirochristopher and @burke . How might this affect backwards compatibility? If a module were to move their DAO logic from Hibernate Criteria API to JPA Criteria API or Spring Data JPA, is this something they could do now and their module would still be able to support legacy OpenMRS versions? Or is this something that would only be something that would work on OpenMRS x.y.z on?

Yeah, backwards compatibility is always going to be the hard part here. It’s always going to be easiest to say “from OpenMRS 2.6.0 on” or something.

Concretely, the challenge is that to use the JPA mechanisms (whether through Spring’s data layer or not) we would need to obtain a JPA EntityManager (the em in @miirochristopher’s examples).

For OpenMRS 2.4+ this is pretty trivial because Hibernate’s default SessionFactory also implements the EntityManagerFactory interface and the default Session implements the EntityManager interface, so an EntityManager can be trivially obtained via casting:

EntityManager em = sessionFactory.getCurrentSession();

For anything prior to that, Hibernate’s JPA implementation is separate from the core ORM, so we’d need a module that implements some conversion between what we have (a Hibernate SessionManager and / or Session) and the equivalent JPA SessionManager or Session, the steps for which might vary based on OpenMRS version. I don’t know exactly what the level of effort is for coming up with something like that.