/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.test.querycache; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.hibernate.Criteria; import org.hibernate.EmptyInterceptor; import org.hibernate.Hibernate; import org.hibernate.Query; import org.hibernate.SQLQuery; import org.hibernate.Session; import org.hibernate.SessionBuilder; import org.hibernate.Transaction; import org.hibernate.cfg.AvailableSettings; import org.hibernate.criterion.Restrictions; import org.hibernate.stat.EntityStatistics; import org.hibernate.stat.QueryStatistics; import org.hibernate.transform.Transformers; import org.hibernate.type.Type; import org.hibernate.testing.DialectChecks; import org.hibernate.testing.RequiresDialectFeature; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * @author Gavin King * @author Brett Meyer */ public class QueryCacheTest extends BaseNonConfigCoreFunctionalTestCase { private static final CompositeKey PK = new CompositeKey(1, 2); private static final ExecutorService executor = Executors.newFixedThreadPool(4); @Override public String[] getMappings() { return new String[] { "querycache/Item.hbm.xml" }; } @Override protected Class[] getAnnotatedClasses() { return new Class[] { CompositeKey.class, EntityWithCompositeKey.class, StringCompositeKey.class, EntityWithStringCompositeKey.class }; } @Override protected void addSettings(Map settings) { settings.put( AvailableSettings.USE_QUERY_CACHE, "true" ); settings.put( AvailableSettings.CACHE_REGION_PREFIX, "foo" ); settings.put( AvailableSettings.USE_SECOND_LEVEL_CACHE, "true" ); settings.put( AvailableSettings.GENERATE_STATISTICS, "true" ); } @Override protected void shutDown() { super.shutDown(); executor.shutdown(); } @Override protected boolean isCleanupTestDataRequired() { return true; } @Override protected String getCacheConcurrencyStrategy() { return "nonstrict-read-write"; } @Test @TestForIssue( jiraKey = "HHH-5426" ) public void testInvalidationFromBulkHQL() { sessionFactory().getCache().evictQueryRegions(); sessionFactory().getStatistics().clear(); Session s = openSession(); List list = new ArrayList(); s.beginTransaction(); for (int i = 0; i < 3; i++) { Item a = new Item(); a.setName("a" + i); a.setDescription("a" + i); list.add(a); s.persist(a); } s.getTransaction().commit(); s.close(); s = openSession(); s.beginTransaction(); String queryString = "select count(*) from Item"; // this query will hit the database and create the cache Long result = (Long) s.createQuery(queryString).setCacheable(true).uniqueResult(); assertEquals(3, result.intValue()); s.getTransaction().commit(); s.close(); s = openSession(); s.beginTransaction(); String updateString = "delete from Item"; s.createQuery(updateString).executeUpdate(); s.getTransaction().commit(); s.close(); s = openSession(); s.beginTransaction(); // and this one SHOULD not be served by the cache Number result2 = (Number) s.createQuery(queryString).setCacheable(true).uniqueResult(); assertEquals(0, result2.intValue()); s.getTransaction().commit(); s.close(); } @Test @TestForIssue( jiraKey = "JBPAPP-4224" ) public void testHitCacheInSameSession() { sessionFactory().getCache().evictQueryRegions(); sessionFactory().getStatistics().clear(); Session s = openSession(); List list = new ArrayList(); s.beginTransaction(); for ( int i = 0; i < 3; i++ ) { Item a = new Item(); a.setName( "a" + i ); a.setDescription( "a" + i ); list.add( a ); s.persist( a ); } s.getTransaction().commit(); // s.close(); // s=openSession(); s.beginTransaction(); String queryString = "from Item"; // this query will hit the database and create the cache s.createQuery( queryString ).setCacheable( true ).list(); s.getTransaction().commit(); s.beginTransaction(); //and this one SHOULD served by the cache s.createQuery( queryString ).setCacheable( true ).list(); s.getTransaction().commit(); QueryStatistics qs = s.getSessionFactory().getStatistics().getQueryStatistics( queryString ); assertEquals( 1, qs.getCacheHitCount() ); assertEquals( 1, qs.getCachePutCount() ); s.close(); s = openSession(); s.beginTransaction(); for(Object obj:list){ s.delete( obj ); } s.getTransaction().commit(); s.close(); } private static final String queryString = "from Item i where i.name='widget'"; @Test public void testQueryCacheInvalidation() throws Exception { sessionFactory().getCache().evictQueryRegions(); sessionFactory().getStatistics().clear(); final String queryString = "from Item i where i.name='widget'"; Session s = openSession(); Transaction t = s.beginTransaction(); s.createQuery( queryString ).setCacheable(true).list(); Item i = new Item(); i.setName("widget"); i.setDescription("A really top-quality, full-featured widget."); s.save(i); t.commit(); s.close(); QueryStatistics qs = s.getSessionFactory().getStatistics().getQueryStatistics( queryString ); EntityStatistics es = s.getSessionFactory().getStatistics().getEntityStatistics( Item.class.getName() ); Thread.sleep(200); s = openSession(); t = s.beginTransaction(); List result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 0 ); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 1 ); assertEquals( s.getSessionFactory().getStatistics().getEntityFetchCount(), 0 ); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); assertTrue( Hibernate.isInitialized( result.get(0) ) ); i = (Item) result.get(0); i.setName("Widget"); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 2 ); assertEquals( qs.getCacheMissCount(), 2 ); assertEquals( s.getSessionFactory().getStatistics().getEntityFetchCount(), 0 ); Thread.sleep(200); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); i = (Item) s.get( Item.class, new Long(i.getId()) ); s.delete(i); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 2 ); assertEquals( qs.getCacheMissCount(), 3 ); assertEquals( qs.getCachePutCount(), 3 ); assertEquals( qs.getExecutionCount(), 3 ); assertEquals( es.getFetchCount(), 0 ); //check that it was being cached } @Test @RequiresDialectFeature( value = DialectChecks.CaseSensitiveCheck.class, comment = "i.name='widget' should not match on case sensitive database." ) public void testCaseInsensitiveComparison() { Session s = openSession(); s.beginTransaction(); Item i = new Item(); i.setName( "Widget" ); i.setDescription( "A really top-quality, full-featured widget." ); s.save( i ); s.getTransaction().commit(); s.close(); s = openSession(); s.beginTransaction(); List result = s.createQuery( queryString ).list(); assertEquals(1, result.size()); i = (Item) s.get( Item.class, new Long(i.getId()) ); assertEquals( i.getName(), "Widget" ); s.delete(i); s.getTransaction().commit(); s.close(); } @Test public void testQueryCacheFetch() throws Exception { sessionFactory().getCache().evictQueryRegions(); sessionFactory().getStatistics().clear(); // persist our 2 items. This saves them to the db, but also into the second level entity cache region Session s = openSession(); Transaction t = s.beginTransaction(); Item i = new Item(); i.setName("widget"); i.setDescription("A really top-quality, full-featured widget."); Item i2 = new Item(); i2.setName("other widget"); i2.setDescription("Another decent widget."); s.persist(i); s.persist(i2); t.commit(); s.close(); final String queryString = "from Item i where i.name like '%widget'"; QueryStatistics qs = s.getSessionFactory().getStatistics().getQueryStatistics( queryString ); Thread.sleep(200); // perform the cacheable query. this will execute the query (no query cache hit), but the Items will be // found in second level entity cache region s = openSession(); t = s.beginTransaction(); List result = s.createQuery( queryString ).setCacheable( true ).list(); assertEquals( result.size(), 2 ); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 0 ); assertEquals( s.getSessionFactory().getStatistics().getEntityFetchCount(), 0 ); // evict the Items from the second level entity cache region sessionFactory().getCache().evictEntityRegion( Item.class ); // now, perform the cacheable query again. this time we should not execute the query (query cache hit). // However, the Items will not be found in second level entity cache region this time (we evicted them above) // nor are they in associated with the session. s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 2 ); assertTrue( Hibernate.isInitialized( result.get(0) ) ); assertTrue( Hibernate.isInitialized( result.get(1) ) ); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 1 ); assertEquals( s.getSessionFactory().getStatistics().getEntityFetchCount(), 1 ); s = openSession(); t = s.beginTransaction(); s.createQuery("delete Item").executeUpdate(); t.commit(); s.close(); } @Test public void testProjectionCache() throws Exception { sessionFactory().getCache().evictQueryRegions(); sessionFactory().getStatistics().clear(); final String queryString = "select i.description as desc from Item i where i.name='widget'"; Session s = openSession(); Transaction t = s.beginTransaction(); s.createQuery( queryString ).setCacheable(true).list(); Item i = new Item(); i.setName("widget"); i.setDescription("A really top-quality, full-featured widget."); s.save(i); t.commit(); s.close(); QueryStatistics qs = s.getSessionFactory().getStatistics().getQueryStatistics( queryString ); EntityStatistics es = s.getSessionFactory().getStatistics().getEntityStatistics( Item.class.getName() ); assertEquals( qs.getCacheHitCount(), 0 ); assertEquals( qs.getCacheMissCount(), 1 ); assertEquals( qs.getCachePutCount(), 1 ); Thread.sleep(200); s = openSession(); t = s.beginTransaction(); List result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); assertEquals( i.getDescription(), ( result.get( 0 ) ) ); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 0 ); assertEquals( qs.getCacheMissCount(), 2 ); assertEquals( qs.getCachePutCount(), 2 ); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); assertEquals( i.getDescription(), result.get( 0 ) ); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 1 ); assertEquals( qs.getCacheMissCount(), 2 ); assertEquals( qs.getCachePutCount(), 2 ); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP).list(); assertEquals( result.size(), 1 ); Map m = (Map) result.get(0); assertEquals( 1, m.size() ); assertEquals( i.getDescription(), m.get( "desc" ) ); t.commit(); s.close(); assertEquals( "hit count should go up since data is not transformed until after it is cached", qs.getCacheHitCount(), 2 ); assertEquals( qs.getCacheMissCount(), 2 ); assertEquals( qs.getCachePutCount(), 2 ); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP).list(); assertEquals( result.size(), 1 ); m = (Map) result.get(0); assertEquals(1, m.size()); assertEquals( i.getDescription(), m.get( "desc" ) ); t.commit(); s.close(); assertEquals( "hit count should go up since data is not transformed until after it is cachedr", qs.getCacheHitCount(), 3 ); assertEquals( qs.getCacheMissCount(), 2 ); assertEquals( qs.getCachePutCount(), 2 ); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); assertTrue( Hibernate.isInitialized( result.get(0) ) ); i = (Item) s.get( Item.class, new Long(i.getId()) ); i.setName("widget"); i.setDescription("A middle-quality widget."); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 4 ); assertEquals( qs.getCacheMissCount(), 2 ); assertEquals( qs.getCachePutCount(), 2 ); Thread.sleep(200); s = openSession(); t = s.beginTransaction(); result = s.createQuery( queryString ).setCacheable(true).list(); assertEquals( result.size(), 1 ); i = (Item) s.get( Item.class, new Long(i.getId()) ); assertEquals( result.get(0), "A middle-quality widget." ); assertEquals( qs.getCacheHitCount(), 4 ); assertEquals( qs.getCacheMissCount(), 3 ); assertEquals( qs.getCachePutCount(), 3 ); s.delete(i); t.commit(); s.close(); assertEquals( qs.getCacheHitCount(), 4 ); assertEquals( qs.getCacheMissCount(), 3 ); assertEquals( qs.getCachePutCount(), 3 ); assertEquals( qs.getExecutionCount(), 3 ); assertEquals( es.getFetchCount(), 0 ); //check that it was being cached } @Test @TestForIssue( jiraKey = "HHH-4459" ) public void testGetByCompositeId() { Session s = openSession(); s.beginTransaction(); s.persist( new EntityWithCompositeKey( PK ) ); Query query = s.createQuery( "FROM EntityWithCompositeKey e WHERE e.pk = :pk" ); query.setCacheable( true ); query.setParameter( "pk", PK ); assertEquals(1, query.list().size( )); s.getTransaction().rollback(); s.close(); s = openSession(); s.beginTransaction(); EntityWithStringCompositeKey entity = new EntityWithStringCompositeKey(); StringCompositeKey key = new StringCompositeKey(); key.setAnalog( "foo1" ); key.setDevice( "foo2" ); key.setDeviceType( "foo3" ); key.setSubstation( "foo4" ); entity.setPk( key ); s.persist( entity ); Criteria c = s.createCriteria( EntityWithStringCompositeKey.class ).add( Restrictions.eq( "pk", key ) ); c.setCacheable( true ); assertEquals( 1, c.list().size() ); s.getTransaction().rollback(); s.close(); } @Test @TestForIssue( jiraKey = "HHH-3051" ) public void testScalarSQLQuery() { sessionFactory().getCache().evictQueryRegions(); sessionFactory().getStatistics().clear(); Session s = openSession(); s.beginTransaction(); Item item = new Item(); item.setName("fooName"); item.setDescription("fooDescription"); s.persist(item); s.getTransaction().commit(); s.close(); s = openSession(); s.beginTransaction(); // Note: StandardQueryCache#put handles single results and multiple results differently. So, test both // 1 and 2+ scalars. String sqlQuery = "select name, description from Items"; SQLQuery query = s.createSQLQuery(sqlQuery); query.setCacheable(true); query.addScalar("name"); query.addScalar("description"); Object[] result1 = (Object[]) query.uniqueResult(); assertNotNull( result1 ); assertEquals( result1.length, 2 ); assertEquals( result1[0], "fooName" ); assertEquals( result1[1], "fooDescription" ); sqlQuery = "select name from Items"; query = s.createSQLQuery(sqlQuery); query.setCacheable(true); query.addScalar("name"); String result2 = (String) query.uniqueResult(); assertNotNull( result2 ); assertEquals( result2, "fooName" ); s.getTransaction().commit(); s.close(); } // @Test // public void testGetByCompositeIdNoCache() { // Query query = em.createQuery("FROM EntityWithCompositeKey e WHERE e.pk = :pk"); // query.setParameter("pk", PK); // assertEquals(1, query.getResultList().size()); // } // // @Test // public void testGetByEntityIself() { // Query query = em.createQuery("FROM EntityWithCompositeKey e WHERE e = :ent"); // query.setParameter("ent", new EntityWithCompositeKey(PK)); // assertEquals(1, query.getResultList().size()); // } @Test @TestForIssue(jiraKey = "HHH-9962") /* Test courtesy of Giambattista Bloisi */ public void testDelayedLoad() throws InterruptedException, ExecutionException { DelayLoadOperations interceptor = new DelayLoadOperations(); final SessionBuilder sessionBuilder = sessionFactory().withOptions().interceptor(interceptor); Item item1 = new Item(); item1.setName("Item1"); item1.setDescription("Washington"); Session s1 = sessionBuilder.openSession(); Transaction tx1 = s1.beginTransaction(); s1.persist(item1); tx1.commit(); s1.close(); Item item2 = new Item(); item2.setName("Item2"); item2.setDescription("Chicago"); Session s2 = sessionBuilder.openSession(); Transaction tx2 = s2.beginTransaction(); s2.persist(item2); tx2.commit(); s2.close(); interceptor.blockOnLoad(); Future<Item> fetchedItem = executor.submit(new Callable<Item>() { public Item call() throws Exception { return findByDescription(sessionBuilder, "Washington"); } }); // wait for the onLoad listener to be called interceptor.waitOnLoad(); Session s3 = sessionBuilder.openSession(); Transaction tx3 = s3.beginTransaction(); item1.setDescription("New York"); item2.setDescription("Washington"); s3.update(item1); s3.update(item2); tx3.commit(); s3.close(); interceptor.unblockOnLoad(); // the concurrent query was executed before the data was amended so // let's expect "Item1" to be returned as living in Washington Item fetched = fetchedItem.get(); assertEquals("Item1", fetched.getName()); // Query again: now "Item2" is expected to live in Washington fetched = findByDescription(sessionBuilder, "Washington"); assertEquals("Item2", fetched.getName()); } protected Item findByDescription(SessionBuilder sessionBuilder, final String description) { Session s = sessionBuilder.openSession(); try { return (Item) s.createCriteria(Item.class) .setCacheable(true) .setReadOnly(true) .add(Restrictions.eq("description", description)) .uniqueResult(); } finally { s.close(); } } public class DelayLoadOperations extends EmptyInterceptor { private volatile CountDownLatch blockLatch; private volatile CountDownLatch waitLatch; @Override public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { // Synchronize load and update activities try { if (waitLatch != null) { waitLatch.countDown(); waitLatch = null; } if (blockLatch != null) { blockLatch.await(); blockLatch = null; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } return true; } public void blockOnLoad() { blockLatch = new CountDownLatch(1); waitLatch = new CountDownLatch(1); } public void waitOnLoad() throws InterruptedException { waitLatch.await(); } public void unblockOnLoad() { if (blockLatch != null) { blockLatch.countDown(); } } } }