What you implemented in your code snippets is a custom cache based on spring-cache. With your implementation, you will need to take care of evicting the cache, making sure that when your graphic objects are cached, they will be loaded correctly, etc. As soon as they receive caching, and the initial hibernation session that loaded them is closed, they will get around, you will no longer be able to navigate through unattached lazy associations. In addition, your custom cache solution in its current state will cache entity diagrams, which is probably not what you want, since any part of this graph may change at a given time, and your cache solution will need to monitor changes in all parts of this schedule handles evictions correctly.
The configuration posted in your question is not a second level cache of Hibernate .
Managing the cache is tricky, and I do not recommend it doing it yourself if you are not sure what you are doing (but then you will not ask this question in Stackoverflow).
Let me explain what happens when you get a LazyInitializationException : you marked one of your dao methods with @org.springframework.cache.annotation.Cacheable . In this case, the following occurs:
- Spring attaches an interceptor to your managed bean. The interceptor will intercept the call to the dao method, it will create a cache key based on the interceptor method and the actual arguments of the method (this can be configured) and find the cache to see if there is any entry in the cache for this key. If there is a record, it will return this record without actually calling your method. If there is no cache entry for this key, it will call your method, serialize the return value and store it in the cache.
- If there is no cache entry for the key, your method will be called. Your method uses the provided spring proxy with one proxy for the associated
EntityManager stream, which was assigned earlier when spring encountered the first call to the @Transactional method. In your case, it was the getContent(...) method of another spring bean service. So your method loads an object with EntityManager.find() . This will give you a partially loaded object graph containing uninitialized proxies and collections for other related objects not yet loaded by the persistence context. - Your method returns with a partially loaded entity graph, and spring immediately serializes it for you and stores it in the cache. Note that serialization of a partially loaded entity graph is deserialized to a partially loaded entity graph.
- The second time the dao method is called, marked with
@Cacheable with the same arguments, spring will detect that there really is a cache entry corresponding to this key and will load and deserialize the record. Your dao method will not be called, since it uses a cached record. Now you are faced with a problem: your deserialized graph of cached objects was partially loaded when stored in the cache, and as soon as you touch any uninitialized part of the graph, you will get a LazyInitializationException . It will always be separated from the deserialized entity, so even if the original EntityManager is still open (which is not the case), you will still get the same exception.
Now the question is: what can you do to avoid a LazyInitializationException . My recommendation is that you forget about implementing a custom cache and simply configure Hibernate to perform caching. I will talk about how to do this later. If you want to stick to the user cache you were trying to implement, here is what you need to do:
Go through your entire code base and find all the calls to the @Cacheable dao method. Follow all possible code paths where the graph of the loaded object is loaded, and mark all parts of the entity graph that are ever affected by client code. Now go back to your @Cacheable method and change it so that it loads and initializes all parts of the entity graph that could ever be affected. Because, as soon as you return it, and it will be serialized, and later deserialized, it will always be in the off state, so itโs better to make sure that all possible paths of the graph are loaded correctly. You should already feel how impractical this will end. If this has not yet convinced you not to follow this direction, here is another argument.
Since you are loading a potentially large fragment of the database, you will have a snapshot of this part of the database at the moment when it is loaded and cached. Now that you are using the cached version of this large fragment of the database, there is a risk that you are using an outdated version of this data. To protect yourself from this, you will need to monitor any changes in the current version of this large fragment of the database that you just cached and evict the entire object graph from the cache. Thus, to a large extent, you need to consider which objects are parts of the graph of your object and configure some event listeners when these entities change and crowd out the entire graph. None of these problems are present in the second-level cache of Hibernate.
Now back to my recommendation: configure the second level cache of Hibernate
Hibernate's Layer 2 cache is managed by Hibernate, and you automatically get sleep eviction control. If you enable Hibernate's second level cache, Hibernate will cache the data needed to restore your objects, and if - when looking for an object to load from the database - it discovers that it has a valid cache entry for your entity, it will skip the database and restoring your essence from the cache. (Note the difference for caching an entity graph with its possible loose associations and uninitialized proxies in your custom cache solution). It will also replace obsolete cache entries when updating an object. It performs all kinds of actions related to cache management, so you do not need to worry about it.
Here you can enable the Hibernate second level cache: in addition to your configuration, do the following:
In addition to the sleep mode properties that you already have to manage at the second level, namely
<entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/> <entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/> <entry key="hibernate.cache.use_second_level_cache" value="true" />
add the following entry:
<entry key="javax.persistence.sharedCache.mode" value="ENABLE_SELECTIVE" />
Alternatively, you can add the shared-cache-mode configuration parameter to your persistence.xml (since you did not publish it, I assumed that you are not using it, therefore, the previous alternative, although this is preferable :)
<persistence-unit name="default"> <!-- other configuration lines stripped --> <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode> <!-- other configuration lines stripped --> </persistence-unit>
- Add
javax.persistence.@Cacheable annotation to the @Entity classes you want to cache. If you want to add caching for collection associations that Hibernate does not cache by default, you can add the @org.hibernate.annotations.Cache annotation (with the right concurrency cache strategy selection) for each such collection:
@ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "product_category", joinColumns = { @JoinColumn(name = "PRODUCT_ID") }, inverseJoinColumns = { @JoinColumn(name = "CATEGORY_ID") }) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) private List<Category> categories;
For more information, see Performance Improvement / L2 Cache in the Hibernate documentation.
This is a good informative article on the topic: Hibernate L2 Cache Traps / Queries
I put together a small small project based on your published code snippets that you can check to see the second level cache in Hibernate.