15.4. Caching query results

We talked about the second-level cache and Hibernate's general cache architecture in chapter 13, section 13.3, "Caching fundamentals." You know that the second-level cache is a shared cache of data, and that Hibernate tries to resolve data through a lookup in this cache whenever you access an unloaded proxy or collection or when you load an object by identifier (these are all identifier lookups, from the point of view of the second-level cache). Query results, on the other hand, are by default not cached.

Some queries still use the second-level cache, depending on how you execute a query. For example, if you decide to execute a query with iterate(), as we showed in the previous chapter, only the primary keys of entities are retrieved from the database, and entity data is looked up through the first-level and, if enabled for a particular entity, second-level cache. We also concluded that this option makes sense only if the second-level cache is enabled, because an optimization of column reads usually doesn't influence performance.

Caching query results is a completely different issue. The query result cache is by default disabled, and every HQL, JPA QL, SQL, and Criteria query always hits the database first. We first show you how to enable the query result cache and how it works. We then discuss why it's disabled and why few queries benefit from result caching.

15.4.1. Enabling the query result cache

The query cache must be enabled using a Hibernate configuration property:

hibernate.cache.use_query_cache = true

However, this setting alone isn't enough for Hibernate to cache query results. By default, all queries always ignore the cache. To enable query caching for a particular query (to allow its results to be added to the cache, and to allow it to draw its results from the cache), you use the org.hibernate.Query interface.

Query categoryByName =
    session.createQuery("from Category c where c.name = :name");
categoryByName.setString("name", categoryName);
categoryByName.setCacheable(true);

The setCachable() method enables the result cache. It's also available on the Criteria API. If you want to enable result caching for a javax.persistence.Query, use setHint("org.hibernate.cacheable", true).

15.4.2. Understanding the query cache

When a query is executed for the first time, its results are cached in a cache region—this region is different from any other entity or collection cache region you may already have configured. The name of the region is by default org.hibernate.cache.QueryCache.

You can change the cache region for a particular query with the setCacheRegion() method:

Query categoryByName =
    session.createQuery("from Category c where c.name = :name");
categoryByName.setString("name", categoryName);
categoryByName.setCacheable(true);
categoryByName.setCacheRegion("my.Region");

This is rarely necessary; you use a different cache region for some queries only if you need a different region configuration—for example, to limit memory consumption of the query cache on a more fine-grained level.

The standard query result cache region holds the SQL statements (including all bound parameters) and the resultset of each SQL statement. This isn't the complete SQL resultset, however. If the resultset contains entity instances (the previous example queries return Category instances), only the identifier values are held in the resultset cache. The data columns of each entity are discarded from the resultset when it's put into the cache region. So, hitting the query result cache means that Hibernate will, for the previous queries, find some Category identifier values.

It's the responsibility of the second-level cache region auction.model.Category (in conjunction with the persistence context) to cache the state of entities. This is similar to the lookup strategy of iterate(), as explained earlier. In other words, if you query for entities and decide to enable caching, make sure you also enabled regular second-level caching for these entities. If you don't, you may end up with more database hits after enabling the query cache.

If you cache the result of a query that doesn't return entity instances, but returns only the same scalar values (e.g., item names and prices), these values are held in the query result cache directly.

If the query result cache is enabled in Hibernate, another always required cache region is also present: org.hibernate.cache.UpdateTimestampsCache. This is a cache region used by Hibernate internally.

Hibernate uses the timestamp region to decide whether a cached query resultset is stale. When you re-execute a query that has caching enabled, Hibernate looks in the timestamp cache for the timestamp of the most recent insert, update, or delete made to the queried table(s). If the found timestamp is later than the timestamp of the cached query results, the cached results are discarded and a new query is issued. This effectively guarantees that Hibernate won't use the cached query result if any table that may be involved in the query contains updated data; hence, the cached result may be stale. For best results, you should configure the timestamp region so that the update timestamp for a table doesn't expire from the cache while query results from these tables are still cached in one of the other regions. The easiest way is to turn off expiry for the timestamp cache region in your second-level cache provider's configuration.

15.4.3. When to use the query cache

The majority of queries don't benefit from result caching. This may come as a surprise. After all, it sounds like avoiding a database hit is always a good thing. There are two good reasons why this doesn't always work for arbitrary queries, compared to object navigation or retrieval by identifier.

First, you must ask how often you're going to execute the same query repeatedly. Granted, you may have a few queries in your application that are executed over and over again, with exactly the same arguments bound to parameters, and the same automatically generated SQL statement. We consider this a rare case, but when you're certain a query is executed repeatedly, it becomes a good candidate for result caching.

Second, for applications that perform many queries and few inserts, deletes, or updates, caching queries can improve performance and scalability. On the other hand if the application performs many writes, the query cache won't be utilized efficiently. Hibernate expires a cached query resultset when there is any insert, update, or delete of any row of a table that appeared in the cached query result. This means cached results may have a short lifetime, and even if a query is executed repeatedly, no cached result can be used due to concurrent modifications of the same data (same tables).

For many queries, the benefit of the query result cache is nonexistent or, at least, doesn't have the impact you'd expect. But one special kind of query can greatly benefit from result caching.

15.4.4. Natural identifier cache lookups

Let's assume that you have an entity that has a natural key. We aren't talking about a natural primary key, but about a business key that applies to a single or compound attributes of your entity. For example, the login name of a user can be a unique business key, if it's immutable. This is the key we already isolated as perfect for the implementation of a good equals() object equality routine. You can find examples of such keys in "Implementing equality with a business key," in chapter 9, section 9.2.3.

Usually, you map the attributes that form your natural key as regular properties in Hibernate. You may enable a unique constraint at the database level to represent this key. For example, if you consider the User class, you may decide that username and emailAddress form the entity's business key:

<class name="User">
    <id name="id".../>

    <property name="username" unique-key="UNQ_USERKEY"/>
    <property name="emailAddress" unique-key="UNQ_USERKEY"/>
    ...

</class>

This mapping enables a unique key constraint at the database level that spans two columns. Let's also assume that the business key properties are immutable. This is unlikely, because you probably allow users to update their email addresses, but the functionality we're presenting now makes sense only if you're dealing with an immutable business key. You map immutability as follows:

<class name="User">
    <id name="id".../>

    <property name="username"
              unique-key="UNQ_USERKEY"
              update="false"/>

    <property name="emailAddress"
              unique-key="UNQ_USERKEY"
              update="false"/>
    ...

</class>

Or, to utilize cache lookups by business key, you can map it with <natural-id>:

<class name="User">
    <id name="id".../>

    <cache usage="read-write"/>

    <natural-id mutable="false">
        <property name="username"/>
        <property name="emailAddress"/>
    </natural-id>
    ...

</class>

This grouping automatically enables the generation of a unique key SQL constraint that spans all grouped properties. If the mutable attribute is set to false, it also prevents updating of the mapped columns. You can now use this business key for cache lookups:

Criteria crit = session.createCriteria(User.class);

crit.add( Restrictions.naturalId()
            .set("username", "johndoe")
            .set("emailAddress", "[email protected]")
        );
crit.setCacheable(true);

User result = (User) crit.uniqueResult();

This criteria query finds a particular user object based on the business key. It results in a second-level cache lookup by business key—remember that this is usually a lookup by primary key and is possible only for retrieval by primary identifier. The business key mapping and Criteria API allow you to express this special second-level cache lookup by business key.

At the time of writing, no Hibernate extension annotation for a natural identifier mapping is available, and HQL doesn't support an equivalent keyword for lookup by business key.

From our point of view, caching at the second-level is an important feature, but it's not the first option when optimizing performance. Errors in the design of queries or an unnecessarily complex part of your object model can't be improved with a "cache it all" approach. If an application performs at an acceptable level only with a hot cache—that is, a full cache after several hours or days runtime—it should be checked for serious design mistakes, unperformant queries, and n+1 select problems. Before you decide to enable any of the query cache options explained here, first review and tune your application following the guidelines presented in "Optimization step by step," in chapter 13, section 13.2.5.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.170.174