JPA: caching queries

23,621

Solution 1

There is a query plan cache in Hibernate. So the HQL is not parsed every time the DAO is called (so #1 really occurs only once in your application life-time). It's QueryPlanCache. It's not heavily documented, as it "just works". But you can find more info here.

Solution 2

Use statically defined named queries. They are more efficient because the JPA persistence provider can translate the JP QL string to SQL once at application startup time, as opposed to every time the query is executed, and are recommended in particular for queries that are executed frequently.

A named query is defined using the @NamedQuery annotation that is typically used on the entity class of the result. In your case, on the Order entity:

@Entity
@NamedQueries({
    @NamedQuery(name="Order.findAll",
                query="SELECT o FROM Order o"),
    @NamedQuery(name="Order.findByPrimaryKey",
                query="SELECT o FROM Order o WHERE o.id = :id"),
    @NamedQuery(name="Order.findByCustomerId",
                query="SELECT o FROM Order o WHERE o.customerId = :customerId")
})
public class Order implements Serializable {
    ...
}

It is also recommended to prefix named queries with the entity name (to have some kind of name space and avoid collisions).

And then in the DAO:

class OrderDao {
    EntityManager em;

    List getOrders(Long customerId) {
        return em.createNamedQuery("Order.findByCustomerId")
                 .setParameter("customerId", customerId);
                 .getResultList();
    }
}

PS: I reused the query you suggested as example but it's somehow weird to have the customerId on the Order, I would expect a Customer instead.

References

  • JPA 1.0 Specification
    • Section 3.6.4 "Named Queries"

Solution 3

NamedQueries is the concept you're looking for.

Solution 4

What you want is a NamedQuery. On your Order entity you put:

@NamedQueries({
    @NamedQuery( name = "getOrderByCustomerId", query = "SELECT o FROM Order o WHERE o.customerId = :customerId")
})

Then in your DAO use em.createNamedQuery("getOrderByCustomerId") instead of recreating the query.

Solution 5

JPA 2.1, section "3.1.1 EntityManager Interface":

The Query, TypedQuery, StoredProcedureQuery, CriteriaBuilder, Metamodel, and EntityTransaction objects obtained from an entity manager are valid while that entity manager is open.

The lesson to take home from this quote is that the enlisted query types can only be cached for as long as the entity manager remains open - which we have no saying about for container-managed entity managers.

Three solutions come to mind. 1) Named queries as others have pointed out. 2) Cache a CriteriaQuery instead and hopefully the provider can toss in some kind of optimizations out of it. 3) Use an application-managed entity manager (that remains open).

Cache a CriteriaQuery

@Stateless
public class OrderRepository
{
    @PersistenceUnit
    EntityManagerFactory emf;

    @PersistenceContext
    EntityManager em;

    private CriteriaQuery<Order> query;

    private Parameter<Long> param;

    @PostConstruct
    private void constructQuery() {
        CriteriaBuilder b = emf.getCriteriaBuilder();
        query = b.createQuery(Order.class);
        param = b.parameter(long.class);
        ...
    }

    public List<Order> findByCustomerKey(long key) {
        return em.createQuery(query)
                 .setParameter(param, key)
                 .getResultList();
    }
}

Use an application-managed entity manager

@Stateless
public class OrderRepository
{
    @PersistenceUnit
    EntityManagerFactory emf;

    private EntityManager em;

    private TypedQuery<Order> query;

    @PostConstruct
    private void initialize() {
        em = emf.createEntityManager();
        query = em.createQuery("SELECT o FROM Order o WHERE o.id = ?1", Order.class);
    }

    public List<Order> findByCustomerKey(long key) {
        try {
            return query.setParameter(1, key)
                        .getResultList();
        }
        finally {
            em.clear(); // returned entities are detached
        }
    }

    @PreDestroy
    private void closeEntityManager() {
        em.close();
    }
}
Share:
23,621
Andrey
Author by

Andrey

C/C++, Java, Haskell, Tcl

Updated on July 11, 2022

Comments

  • Andrey
    Andrey almost 2 years

    I'm using JPA to load and persist entities in my Java EE-based web application. Hibernate is used as an implementation of JPA, but I don't use Hibernate-specific features and only work with pure JPA.

    Here is some DAO class, notice getOrders method:

    class OrderDao {
      EntityManager em;
    
      List getOrders(Long customerId) {
        Query q = em.createQuery(
          "SELECT o FROM Order o WHERE o.customerId = :customerId");
        q.setParameter("customerId", customerId);
        return q.getResultList();
      }
    }

    Method is pretty simple but it has a big drawback. Each time the method is called following actions are performed somewhere within JPA implementation:

    1. JPQL expression is parsed and compiled to SQL.
    2. Either Statement or PreparedStatement instance is created and initialized.
    3. Statement instance is filled with parameters and executed.

    I believe that steps 1 and 2 of above should be implemented once per application lifetime. But how to make it? In other words, I need that Query instances to be cached.

    Of course I can implement such a cache on my side. But wait, I am using modern powerful ORM's! Didn't they already made this for me?

    Notice that I'm not mentioning something like Hibernate query cache which caches result of queries. Here I'd like to execute my queries a bit more quickly.

  • Andrey
    Andrey over 13 years
    Thanks, NamedQueries concept is what I should to learn. Unfortunately a lot of my queries are built in runtime. Is there a way to "prepare" a run-time built query in JPA?
  • Andrey
    Andrey over 13 years
    Thank you, that's cool. So can I simply be happy with this fact or I have to turn on this QueryPlanCache in Hibernate configuration?
  • Daniel Bleisteiner
    Daniel Bleisteiner over 13 years
    Named queries also have the advantage of being validated during server startup which will throw errors when the query does not work due to changes to your model. This protects you from being surprised by a rare query not working anytime later when its actually used.
  • Devanshu Mevada
    Devanshu Mevada over 13 years
    @Andrey: The behavior with other queries than named queries is implementation specific (what is true with Hibernate might not apply with another JPA provider).
  • Devanshu Mevada
    Devanshu Mevada over 13 years
    @Daniel Yes, absolutely, that's another benefit.
  • Thierry Roy
    Thierry Roy over 13 years
    No, it's part of your framework since Hibernate is a "modern powerful ORM"! :)
  • chrismarx
    chrismarx almost 9 years
    are queries generated through the criteria api prepared beforehand so their plans can be cached?
  • chrismarx
    chrismarx almost 9 years
    I'm using named queries, and I can see them all get processed at startup, which is wonderful. But what about specification driven jpa queries? It seems like the query execution plans are run every time. Is there no way to cache those plan as well?
  • ichalos
    ichalos almost 9 years
    No, it has nothing to do with the plans. Besides, the execution plan is decided by the database, not JPA.
  • chrismarx
    chrismarx almost 9 years
    Not the execution plan in the db, but the when you use prepared queries, the sql is parsed out beforehand, that's what I'm referring to here
  • ichalos
    ichalos almost 9 years
    Definitely, the sql queries are prepared beforehand by the JPA.
  • Tomas Lukac
    Tomas Lukac over 4 years
    em.clear() is the only solution that worked for me. Thank you so much.
  • ama
    ama about 2 years
    That is awesome, as I find NamedQueris such a "clutter" on top of Entities...