Question

I'm blocked since a few days with Hibernate caching in a Spring context using @Transactional annotations.

I tried all the solutions found on the web without success...

The single solution that work is to use the @Cacheable Spring annotation (from spring-context-support), but I'm not satisfied because I cannot use the Hibernate @Cache annotation on my entities. @Cacheable can just be used on methods like services methods, and I retrieve entities without methods...

For exemple:

I call the following service that fetch a CollectionEntity

@Override
@Transactional(readOnly = true)
public CollectionEntity getById(Integer collectionId) throws Exception {
    if(collectionId < 1) {
        logger.debug("Cannot retrieve a collection from identifier inferior to 1.");
        return null;
    }
    return collectionDao.getById(collectionId);
}

A CollectionEntity contains a ProgramEntity set

@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = CollectionProgram.TABLE, joinColumns = { @JoinColumn(name = COLUMN_COLLECTION_ID, referencedColumnName = CollectionProgram.COLUMN_COLLECTION_ID) }, inverseJoinColumns = { @JoinColumn(name = CollectionProgram.COLUMN_PROGRAM_ID, referencedColumnName = ProgramEntity.COLUMN_PROGRAM_ID) })
private Set<ProgramEntity> programs = new HashSet<ProgramEntity>(0);

These programs contain a ProgramBroadcasting set

@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = ProgramBroadcastingEntity.COLUMN_PROGRAM_ID)
private Set<ProgramBroadcastingEntity> broadcastings = new HashSet<ProgramBroadcastingEntity>(0);

And these programs broadcasting contain a ChannelEntity (a referential data)

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = COLUMN_CHANNEL_ID, nullable = false)
private ChannelEntity channel;

So if I want to cache the ChannelEntity, I normally just need to put the following annotation on its class.

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "Channel")
@Table(name = ChannelEntity.TABLE)
public class ChannelEntity implements java.io.Serializable {

The "EAGER" fetching is a very attractive solution for referential data! But if a want to use @Cacheable, which is at the moment the only solution, I must declare the ChannelEntity with FetchType.LAZY and write a service class to put the @Cacheable on it just to cache this data. It's a joke... I won't do it for all my referential data classes...

A real solution is just to have a working Hibernate @Cache annotation to put on the ChannelEntity.

To get it functional, I even have developed my own "SingletonEhCacheRegionFactory" class initialize with the Spring class "EhCacheManagerFactoryBean" as cache manager. And the data continue to be fetched from database. This solution worked for one of my colleagues, but with older versions of Spring (<4) and Hibernate (<4). So, with the new versions, it doesn't seem to be a good solution...

So, I really need your help.

Before to give you my configuration, here are my quick tests on a main class to retrieve a ChannelEntity from database and then from cache.

Here the test is OK (it work):

private static void testGetChannelFromCache2(BeanFactory factory) throws Exception {
    SessionFactoryImpl sessionFactoryImpl = ((SessionFactoryImpl) factory.getBean("sessionFactory"));
    Session session = sessionFactoryImpl.openSession();
    session.beginTransaction();

    ChannelEntity channel1 = (ChannelEntity) session.load(ChannelEntity.class, new Integer(1));
    System.out.println(channel1.getLabel());

    session.getTransaction().commit();
    session.close();

    Session anotherSession = sessionFactoryImpl.openSession();
    anotherSession.beginTransaction();

    // Here I put a breakpoint and I update the value directly on database.

    channel1 = (ChannelEntity) anotherSession.load(ChannelEntity.class, new Integer(1));
    System.out.println(channel1.getLabel()); // Here I print the cached value, not the new database value. Good!

    anotherSession.getTransaction().commit();
    anotherSession.close();
}

But it's not the real context. On my service layer, I don't manipulate directly the transaction, I used the @Transactional Spring annotation. Here is a more realistic test:

private static void testGetChannelFromCache1(BeanFactory factory) throws Exception {
    ChannelService service = (ChannelService) factory.getBean("channelServiceImpl");
    ChannelEntity entity1 = service.getChannelByCode(ChannelCode.ARTE);
    if(entity1 != null) {
        System.out.println(entity1.getLabel());
    }

    // Here I put a breakpoint and I update the value directly on database.

    ChannelEntity entity2 = service.getChannelByCode(ChannelCode.ARTE);
    if(entity2 != null) {
        System.out.println(entity2.getLabel()); // Here I print the new database value, not the cached value. Not good...
    }
}

Here is the ChannelService:

@Service
@Transactional(rollbackFor = Exception.class)
public class ChannelServiceImpl implements ChannelService {

    @Log
    private Logger logger;

    @Inject
    private ChannelDao channelDao;

    @Override
    @Transactional(readOnly = true)
    public ChannelEntity getChannelByCode(final ChannelCode code) throws Exception {
        if(code == null) {
            logger.debug("Cannot find Channel from null code.");
            return null;
        }
        return channelDao.getByCode(code);
    }
}

Now, my configuration...

Dependencies:

<!-- Hibernate -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
</dependency>

<!-- Ehcache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
</dependency>

Hibernate configuration:

hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
hibernate.cache.use_second_level_cache=true
hibernate.cache.use_query_cache=true
hibernate.cache.generate_statistics=true
net.sf.ehcache.configurationResourceName=/config/ehcache/ehcache.xml

EHCache configuration:

<cache name="Channel"
       maxEntriesLocalHeap="10000"
       eternal="true"
       overflowToDisk="false">

And I use Spring 4.0.3.RELEASE, Hibernate 4.3.4.Final and EHCache 2.6.8.

To be more accurate, it seems that the @Cache Hibernate annotation work, but not completely... In fact, I put several breakpoints on the Hibernate source code and I noted that Hibernate put the ChannelEntity on the cache and after my second call to the ChannelService do a get on the cache and retrieve the channel entity! BUT..., Hibernate still execute the following database request and retrieve the database value. Really strange!

select this_.ChannelId as ChannelI1_3_0_, this_.Code as Code2_3_0_, this_.Label as Label3_3_0_ from Channel this_ where this_.Code=?

Anyone has an idea about this strange behaviour??

Thanks a lot for you help!

Was it helpful?

Solution

The second level and query caches have a few gotchas that look like strange behaviour but it's actually normal.

For example @Cache on entities will only cache entities loaded by their Id. If you want to cache the result of queries other than load by Id, you need to mark the query as cacheable:

@NamedQuery(name="account.queryName",
   query="select acct from Account ...",
   hints={
       @QueryHint(name="org.hibernate.cacheable",
       value="true")
   }     
})

or in the case of criteria queries:

List cats = session.createCriteria(Cat.class)
    .setCacheable(true)
    .list();

Also one to many relations by default are not cached. If you want to cache associations, you need to mark them independently with @Cache. For example:

@Cache(CacheConcurrencyStrategy.READ_WRITE)
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<ProgramEntity> programs = new HashSet<ProgramEntity>(0);

OTHER TIPS

That's really cool jhadesdev!

I defined EHCache caches for my entities, their FetchType.EAGER attributes and for my queries.

Then, I added @Cache annotation on the entities and on their FetchType.EAGER attributes. I also added the "cacheable" attribute on the services allowing to retrieve the values of my entities attributes defined as FetchType.LAZY and on my others queries.

Everything works fine!!

A big thank you to give me the missing concept to implement the cache!!! :-)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top