HHH-4910 The collection cache is evicted if a related collection element is inserted, removed or updated

HHH-4910 minor fixes

HHH-4910 make this feature configurable
This commit is contained in:
Andy2003 2013-09-06 10:09:06 +02:00 committed by Brett Meyer
parent 66b222c8bf
commit bcd6185809
13 changed files with 537 additions and 6 deletions

View File

@ -0,0 +1,170 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2013, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.cache.internal;
import java.io.Serializable;
import java.util.Set;
import org.jboss.logging.Logger;
import org.hibernate.cache.spi.CacheKey;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostDeleteEvent;
import org.hibernate.event.spi.PostDeleteEventListener;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.metamodel.source.MetadataImplementor;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
/**
* @author Andreas Berger (latest modification by $Author$)
* @version $Id$
* @created 30.08.13 - 01:54
*/
public class CollectionCacheInvalidator implements Integrator,
PostInsertEventListener,
PostDeleteEventListener,
PostUpdateEventListener {
private static final Logger LOG = Logger.getLogger( CollectionCacheInvalidator.class.getName() );
@Override
public void integrate(
Configuration configuration,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
integrate( serviceRegistry, sessionFactory );
}
@Override
public void integrate(
MetadataImplementor metadata,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
integrate( serviceRegistry, sessionFactory );
}
@Override
public void disintegrate(
SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
}
@Override
public void onPostInsert(PostInsertEvent event) {
evictCache( event.getEntity(), event.getPersister(), event.getSession(), null );
}
@Override
public boolean requiresPostCommitHanding(EntityPersister persister) {
return true;
}
@Override
public void onPostDelete(PostDeleteEvent event) {
evictCache( event.getEntity(), event.getPersister(), event.getSession(), null );
}
@Override
public void onPostUpdate(PostUpdateEvent event) {
evictCache( event.getEntity(), event.getPersister(), event.getSession(), event.getOldState() );
}
private void integrate(SessionFactoryServiceRegistry serviceRegistry, SessionFactoryImplementor sessionFactory) {
if ( !sessionFactory.getSettings().isAutoEvictCollectionCache() ) {
// feature is disabled
return;
}
if ( !sessionFactory.getSettings().isSecondLevelCacheEnabled() ) {
// Nothing to do, if caching is disabled
return;
}
EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class );
eventListenerRegistry.appendListeners( EventType.POST_INSERT, this );
eventListenerRegistry.appendListeners( EventType.POST_DELETE, this );
eventListenerRegistry.appendListeners( EventType.POST_UPDATE, this );
}
private void evictCache(Object entity, EntityPersister persister, EventSource session, Object[] oldState) {
try {
SessionFactoryImplementor factory = persister.getFactory();
Set<String> collectionRoles = factory.getCollectionRolesByEntityParticipant( persister.getEntityName() );
if ( collectionRoles == null || collectionRoles.isEmpty() ) {
return;
}
for ( String role : collectionRoles ) {
CollectionPersister collectionPersister = factory.getCollectionPersister( role );
if ( !collectionPersister.hasCache() ) {
// ignore collection if no caching is used
continue;
}
// this is the property this OneToMany relation is mapped by
String mappedBy = collectionPersister.getMappedByProperty();
if ( mappedBy != null ) {
int i = persister.getEntityMetamodel().getPropertyIndex( mappedBy );
Serializable oldId = null;
if ( oldState != null ) {
// in case of updating an entity we perhaps have to decache 2 entity collections, this is the old one
oldId = session.getIdentifier( oldState[i] );
}
Object ref = persister.getPropertyValue( entity, i );
Serializable id = null;
if ( ref != null ) {
id = session.getIdentifier( ref );
}
// only evict if the related entity has changed
if ( id != null && !id.equals( oldId ) ) {
evict( id, collectionPersister, session );
if ( oldId != null ) {
evict( oldId, collectionPersister, session );
}
}
}
else {
LOG.debug( "Evict CollectionRegion " + role );
collectionPersister.getCacheAccessStrategy().evictAll();
}
}
}
catch (Exception e) {
//don't let decaching influence other logic
LOG.error( "", e );
}
}
private void evict(Serializable id, CollectionPersister collectionPersister, EventSource session) {
LOG.debug( "Evict CollectionRegion " + collectionPersister.getRole() + " for id " + id );
CacheKey key = session.generateCacheKey( id, collectionPersister.getKeyType(), collectionPersister.getRole() );
collectionPersister.getCacheAccessStrategy().evict( key );
}
}

View File

@ -325,7 +325,10 @@ public interface AvailableSettings {
* Enable use of structured second-level cache entries
*/
public static final String USE_STRUCTURED_CACHE = "hibernate.cache.use_structured_entries";
/**
* Enables the eviction of the OneToMany-Site-Cache of a bi-directional association, when the ManyToOne site changes
*/
public static final String AUTO_EVICT_COLLECTION_CACHE = "hibernate.cache.auto_evict_collection_cache";
/**
* Enable statistics collection
*/

View File

@ -62,6 +62,7 @@ public final class Settings {
private boolean queryCacheEnabled;
private boolean structuredCacheEntriesEnabled;
private boolean secondLevelCacheEnabled;
private boolean autoEvictCollectionCache;
private String cacheRegionPrefix;
private boolean minimalPutsEnabled;
private boolean commentsEnabled;
@ -518,4 +519,12 @@ public final class Settings {
public void setJtaTrackByThread(boolean jtaTrackByThread) {
this.jtaTrackByThread = jtaTrackByThread;
}
public boolean isAutoEvictCollectionCache() {
return autoEvictCollectionCache;
}
public void setAutoEvictCollectionCache(boolean autoEvictCollectionCache) {
this.autoEvictCollectionCache = autoEvictCollectionCache;
}
}

View File

@ -325,6 +325,12 @@ public class SettingsFactory implements Serializable {
}
settings.setDirectReferenceCacheEntriesEnabled( useDirectReferenceCacheEntries );
boolean autoEvictCollectionCache = ConfigurationHelper.getBoolean( AvailableSettings.AUTO_EVICT_COLLECTION_CACHE, properties, false);
if ( debugEnabled ) {
LOG.debugf( "Automatic eviction of collection cache: %s", enabledDisabled(autoEvictCollectionCache) );
}
settings.setAutoEvictCollectionCache( autoEvictCollectionCache );
//Statistics and logging:
boolean useStatistics = ConfigurationHelper.getBoolean( AvailableSettings.GENERATE_STATISTICS, properties );

View File

@ -398,6 +398,7 @@ public abstract class CollectionBinder {
LOG.debugf( "Collection role: %s", role );
collection.setRole( role );
collection.setNodeName( propertyName );
collection.setMappedByProperty( mappedBy );
if ( property.isAnnotationPresent( MapKeyColumn.class )
&& mapKeyPropertyName != null ) {

View File

@ -27,6 +27,7 @@ import java.util.LinkedHashSet;
import org.jboss.logging.Logger;
import org.hibernate.cache.internal.CollectionCacheInvalidator;
import org.hibernate.cfg.beanvalidation.BeanValidationIntegrator;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.integrator.spi.IntegratorService;
@ -46,6 +47,7 @@ public class IntegratorServiceImpl implements IntegratorService {
// separate project/jars.
addIntegrator( new BeanValidationIntegrator() );
addIntegrator( new JaccIntegrator() );
addIntegrator( new CollectionCacheInvalidator() );
// register provided integrators
for ( Integrator integrator : providedIntegrators ) {

View File

@ -72,6 +72,7 @@ public abstract class Collection implements Fetchable, Value, Filterable {
private String referencedPropertyName;
private String nodeName;
private String elementNodeName;
private String mappedByProperty;
private boolean sorted;
private Comparator comparator;
private String comparatorClassName;
@ -667,4 +668,12 @@ public abstract class Collection implements Fetchable, Value, Filterable {
public String getComparatorClassName() {
return comparatorClassName;
}
public String getMappedByProperty() {
return mappedByProperty;
}
public void setMappedByProperty(String mappedByProperty) {
this.mappedByProperty = mappedByProperty;
}
}

View File

@ -109,7 +109,7 @@ import org.jboss.logging.Logger;
/**
* Base implementation of the <tt>QueryableCollection</tt> interface.
*
*
* @author Gavin King
* @see BasicCollectionPersister
* @see OneToManyPersister
@ -149,6 +149,7 @@ public abstract class AbstractCollectionPersister
private final String nodeName;
private final String elementNodeName;
private final String indexNodeName;
private String mappedByProperty;
protected final boolean indexContainsFormula;
protected final boolean elementIsPureFormula;
@ -266,6 +267,7 @@ public abstract class AbstractCollectionPersister
queryLoaderName = collection.getLoaderName();
nodeName = collection.getNodeName();
isMutable = collection.isMutable();
mappedByProperty = collection.getMappedByProperty();
Table table = collection.getCollectionTable();
fetchMode = collection.getElement().getFetchMode();
@ -1642,14 +1644,14 @@ public abstract class AbstractCollectionPersister
protected abstract int doUpdateRows(Serializable key, PersistentCollection collection, SessionImplementor session)
throws HibernateException;
public void processQueuedOps(PersistentCollection collection, Serializable key, SessionImplementor session)
throws HibernateException {
if ( collection.hasQueuedOperations() ) {
doProcessQueuedOps( collection, key, session );
}
}
protected abstract void doProcessQueuedOps(PersistentCollection collection, Serializable key, SessionImplementor session)
throws HibernateException;
@ -1923,7 +1925,7 @@ public abstract class AbstractCollectionPersister
/**
* Intended for internal use only. In fact really only currently used from
* test suite for assertion purposes.
*
*
* @return The default collection initializer for this persister/collection.
*/
public CollectionInitializer getInitializer() {
@ -1934,6 +1936,10 @@ public abstract class AbstractCollectionPersister
return batchSize;
}
public String getMappedByProperty() {
return mappedByProperty;
}
private class StandardOrderByAliasResolver implements OrderByAliasResolver {
private final String rootAlias;
@ -1952,7 +1958,7 @@ public abstract class AbstractCollectionPersister
}
}
}
public abstract FilterAliasGenerator getFilterAliasGenerator(final String rootAlias);
// ColectionDefinition impl ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -319,4 +319,9 @@ public interface CollectionPersister extends CollectionDefinition {
public boolean elementExists(Serializable key, Object element, SessionImplementor session);
public Object getElementByIndex(Serializable key, Object index, SessionImplementor session, Object owner);
public int getBatchSize();
/**
* @return the Name of the Property, this collection is mapped by
*/
public String getMappedByProperty();
}

View File

@ -0,0 +1,180 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2013, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.cache;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.junit.Test;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* @author Andreas Berger
*/
@TestForIssue(jiraKey = "HHH-4910")
public class CollectionCacheEvictionTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {User.class, Company.class};
}
@Override
protected void configure(Configuration cfg) {
super.configure( cfg );
cfg.setProperty( Environment.AUTO_EVICT_COLLECTION_CACHE, "true" );
cfg.setProperty( Environment.USE_SECOND_LEVEL_CACHE, "true" );
cfg.setProperty( Environment.USE_QUERY_CACHE, "true" );
cfg.setProperty( Environment.CACHE_PROVIDER_CONFIG, "true" );
}
@Override
protected void prepareTest() throws Exception {
Session s = openSession();
s.beginTransaction();
Company company1 = new Company( 1 );
s.save( company1 );
User user = new User( 1, company1 );
s.save( user );
Company company2 = new Company( 2 );
s.save( company2 );
s.getTransaction().commit();
s.close();
}
@Override
protected void cleanupTest() throws Exception {
Session s = openSession();
s.beginTransaction();
s.createQuery( "delete from org.hibernate.test.cache.User" ).executeUpdate();
s.createQuery( "delete from org.hibernate.test.cache.Company" ).executeUpdate();
s.getTransaction().commit();
s.close();
}
@Test
public void testCollectionCacheEvictionInsert() {
Session s = openSession();
s.beginTransaction();
Company company = (Company) s.get( Company.class, 1 );
// init cache of collection
assertEquals( 1, company.getUsers().size() );
User user = new User( 2, company );
s.save( user );
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
company = (Company) s.get( Company.class, 1 );
// fails if cache is not evicted
assertEquals( 2, company.getUsers().size() );
s.close();
}
@Test
public void testCollectionCacheEvictionRemove() {
Session s = openSession();
s.beginTransaction();
Company company = (Company) s.get( Company.class, 1 );
// init cache of collection
assertEquals( 1, company.getUsers().size() );
s.delete( company.getUsers().get( 0 ) );
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
company = (Company) s.get( Company.class, 1 );
// fails if cache is not evicted
try {
assertEquals( 0, company.getUsers().size() );
}
catch (ObjectNotFoundException e) {
fail( "Cached element not found" );
}
s.close();
}
@Test
public void testCollectionCacheEvictionUpdate() {
Session s = openSession();
s.beginTransaction();
Company company1 = (Company) s.get( Company.class, 1 );
Company company2 = (Company) s.get( Company.class, 2 );
// init cache of collection
assertEquals( 1, company1.getUsers().size() );
assertEquals( 0, company2.getUsers().size() );
User user = (User) s.get( User.class, 1 );
user.setCompany( company2 );
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
company1 = (Company) s.get( Company.class, 1 );
company2 = (Company) s.get( Company.class, 2 );
assertEquals( 1, company2.getUsers().size() );
// fails if cache is not evicted
try {
assertEquals( 0, company1.getUsers().size() );
}
catch (ObjectNotFoundException e) {
fail( "Cached element not found" );
}
s.close();
}
}

View File

@ -0,0 +1,68 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2013, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.cache;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
public class Company {
@Id
int id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "company")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
List<User> users = new ArrayList<User>();
public Company() {
}
public Company(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}

View File

@ -0,0 +1,67 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2013, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.cache;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class User {
@Id
int id;
@ManyToOne(fetch = FetchType.LAZY)
Company company;
public User() {
}
public User(int id) {
this.id = id;
}
public User(int id, Company company) {
this.id = id;
this.company = company;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Company getCompany() {
return company;
}
public void setCompany(Company group) {
this.company = group;
}
}

View File

@ -850,6 +850,11 @@ public class GoofyPersisterClassProvider implements PersisterClassResolver {
return 0;
}
@Override
public String getMappedByProperty() {
return null;
}
@Override
public void processQueuedOps(PersistentCollection collection, Serializable key, SessionImplementor session)
throws HibernateException {