HHH-12282 - Allow disabling of invalidation of second-level cache entries for multi-table entities

This commit is contained in:
Steve Ebersole 2018-02-13 14:28:03 -06:00
parent 5e397e9cb3
commit 8cfe4126f1
9 changed files with 326 additions and 5 deletions

View File

@ -1212,6 +1212,14 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
this.jpaCompliance.setClosedCompliance( enabled ); this.jpaCompliance.setClosedCompliance( enabled );
} }
public void enableJpaProxyCompliance(boolean enabled) {
this.jpaCompliance.setProxyCompliance( enabled );
}
public void enableJpaCachingCompliance(boolean enabled) {
this.jpaCompliance.setCachingCompliance( enabled );
}
public void markAsJpaBootstrap() { public void markAsJpaBootstrap() {
this.jpaBootstrap = true; this.jpaBootstrap = true;
} }

View File

@ -1834,6 +1834,12 @@ public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings {
*/ */
String JPA_PROXY_COMPLIANCE = "hibernate.jpa.compliance.proxy"; String JPA_PROXY_COMPLIANCE = "hibernate.jpa.compliance.proxy";
/**
* @see JpaCompliance#isJpaCacheComplianceEnabled()
* @since 5.3
*/
String JPA_CACHING_COMPLIANCE = "hibernate.jpa.compliance.caching";
/** /**
* True/False setting indicating if the value stored in the table used by the {@link javax.persistence.TableGenerator} * True/False setting indicating if the value stored in the table used by the {@link javax.persistence.TableGenerator}
* is the last value generated or the next value to be used. * is the last value generated or the next value to be used.

View File

@ -77,4 +77,16 @@ public interface JpaCompliance {
* @return {@code true} indicates to behave in the spec-defined way * @return {@code true} indicates to behave in the spec-defined way
*/ */
boolean isJpaProxyComplianceEnabled(); boolean isJpaProxyComplianceEnabled();
/**
* Should Hibernate comply with all aspects of caching as defined by JPA? Or can
* it deviate to perform things it believes will be "better"?
*
* @implNote Effects include marking all secondary tables as non-optional. The reason
* being that optional secondary tables can lead to entity cache being invalidated rather
* than updated.
*
* @return {@code true} says to act the spec-defined way.
*/
boolean isJpaCacheComplianceEnabled();
} }

View File

@ -21,6 +21,7 @@ public class JpaComplianceImpl implements JpaCompliance {
private boolean listCompliance; private boolean listCompliance;
private boolean closedCompliance; private boolean closedCompliance;
private boolean proxyCompliance; private boolean proxyCompliance;
private boolean cachingCompliance;
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@ -52,6 +53,11 @@ public class JpaComplianceImpl implements JpaCompliance {
configurationSettings, configurationSettings,
jpaByDefault jpaByDefault
); );
cachingCompliance = ConfigurationHelper.getBoolean(
AvailableSettings.JPA_CACHING_COMPLIANCE,
configurationSettings,
jpaByDefault
);
} }
@Override @Override
@ -79,6 +85,11 @@ public class JpaComplianceImpl implements JpaCompliance {
return proxyCompliance; return proxyCompliance;
} }
@Override
public boolean isJpaCacheComplianceEnabled() {
return cachingCompliance;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Mutators // Mutators
@ -97,4 +108,12 @@ public class JpaComplianceImpl implements JpaCompliance {
public void setClosedCompliance(boolean closedCompliance) { public void setClosedCompliance(boolean closedCompliance) {
this.closedCompliance = closedCompliance; this.closedCompliance = closedCompliance;
} }
public void setProxyCompliance(boolean proxyCompliance) {
this.proxyCompliance = proxyCompliance;
}
public void setCachingCompliance(boolean cachingCompliance) {
this.cachingCompliance = cachingCompliance;
}
} }

View File

@ -95,6 +95,7 @@ import org.hibernate.loader.entity.UniqueEntityLoader;
import org.hibernate.mapping.Column; import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component; import org.hibernate.mapping.Component;
import org.hibernate.mapping.Formula; import org.hibernate.mapping.Formula;
import org.hibernate.mapping.Join;
import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property; import org.hibernate.mapping.Property;
import org.hibernate.mapping.Selectable; import org.hibernate.mapping.Selectable;
@ -152,6 +153,7 @@ public abstract class AbstractEntityPersister
private final SessionFactoryImplementor factory; private final SessionFactoryImplementor factory;
private final boolean canReadFromCache; private final boolean canReadFromCache;
private final boolean canWriteToCache; private final boolean canWriteToCache;
private final boolean invalidateCache;
private final EntityRegionAccessStrategy cacheAccessStrategy; private final EntityRegionAccessStrategy cacheAccessStrategy;
private final NaturalIdRegionAccessStrategy naturalIdRegionAccessStrategy; private final NaturalIdRegionAccessStrategy naturalIdRegionAccessStrategy;
private final boolean isLazyPropertiesCacheable; private final boolean isLazyPropertiesCacheable;
@ -845,6 +847,50 @@ public abstract class AbstractEntityPersister
this.cacheEntryHelper = buildCacheEntryHelper(); this.cacheEntryHelper = buildCacheEntryHelper();
if ( creationContext.getSessionFactory().getSessionFactoryOptions().isSecondLevelCacheEnabled() ) {
this.invalidateCache = canWriteToCache && determineWhetherToInvalidateCache( persistentClass, creationContext );
}
else {
this.invalidateCache = false;
}
}
@SuppressWarnings("RedundantIfStatement")
private boolean determineWhetherToInvalidateCache(
PersistentClass persistentClass,
PersisterCreationContext creationContext) {
if ( hasFormulaProperties() ) {
return true;
}
if ( isVersioned() ) {
return false;
}
if ( entityMetamodel.isDynamicUpdate() ) {
return false;
}
// We need to check whether the user may have circumvented this logic (JPA TCK)
final boolean complianceEnabled = creationContext.getSessionFactory()
.getSessionFactoryOptions()
.getJpaCompliance()
.isJpaCacheComplianceEnabled();
if ( complianceEnabled ) {
// The JPA TCK (inadvertently, but still...) requires that we cache
// entities with secondary tables even though Hibernate historically
// invalidated them
return false;
}
if ( persistentClass.getJoinClosureSpan() >= 1 ) {
// todo : this should really consider optionality of the secondary tables in count
// non-optional tables do not cause this bypass
return true;
}
return false;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -1273,8 +1319,7 @@ public abstract class AbstractEntityPersister
* item. * item.
*/ */
public boolean isCacheInvalidationRequired() { public boolean isCacheInvalidationRequired() {
return hasFormulaProperties() || return invalidateCache;
( !isVersioned() && ( entityMetamodel.isDynamicUpdate() || getTableSpan() > 1 ) );
} }
public boolean isLazyPropertiesCacheable() { public boolean isLazyPropertiesCacheable() {

View File

@ -243,7 +243,12 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister {
while ( joinItr.hasNext() ) { while ( joinItr.hasNext() ) {
Join join = (Join) joinItr.next(); Join join = (Join) joinItr.next();
isNullableTable[tableIndex++] = join.isOptional(); isNullableTable[tableIndex++] = join.isOptional() ||
creationContext.getSessionFactory()
.getSessionFactoryOptions()
.getJpaCompliance()
.isJpaCacheComplianceEnabled();
Table table = join.getTable(); Table table = join.getTable();
final String tableName = determineTableName( table, jdbcEnvironment ); final String tableName = determineTableName( table, jdbcEnvironment );

View File

@ -178,7 +178,11 @@ public class SingleTableEntityPersister extends AbstractEntityPersister {
Join join = (Join) joinIter.next(); Join join = (Join) joinIter.next();
qualifiedTableNames[j] = determineTableName( join.getTable(), jdbcEnvironment ); qualifiedTableNames[j] = determineTableName( join.getTable(), jdbcEnvironment );
isInverseTable[j] = join.isInverse(); isInverseTable[j] = join.isInverse();
isNullableTable[j] = join.isOptional(); isNullableTable[j] = join.isOptional()
|| creationContext.getSessionFactory()
.getSessionFactoryOptions()
.getJpaCompliance()
.isJpaCacheComplianceEnabled();
cascadeDeleteEnabled[j] = join.getKey().isCascadeDeleteEnabled() && cascadeDeleteEnabled[j] = join.getKey().isCascadeDeleteEnabled() &&
factory.getDialect().supportsCascadeDelete(); factory.getDialect().supportsCascadeDelete();
@ -244,7 +248,12 @@ public class SingleTableEntityPersister extends AbstractEntityPersister {
isConcretes.add( persistentClass.isClassOrSuperclassJoin( join ) ); isConcretes.add( persistentClass.isClassOrSuperclassJoin( join ) );
isDeferreds.add( join.isSequentialSelect() ); isDeferreds.add( join.isSequentialSelect() );
isInverses.add( join.isInverse() ); isInverses.add( join.isInverse() );
isNullables.add( join.isOptional() ); isNullables.add(
join.isOptional() || creationContext.getSessionFactory()
.getSessionFactoryOptions()
.getJpaCompliance()
.isJpaCacheComplianceEnabled()
);
isLazies.add( lazyAvailable && join.isLazy() ); isLazies.add( lazyAvailable && join.isLazy() );
if ( join.isSequentialSelect() && !persistentClass.isClassOrSuperclassJoin( join ) ) { if ( join.isSequentialSelect() && !persistentClass.isClassOrSuperclassJoin( join ) ) {
hasDeferred = true; hasDeferred = true;

View File

@ -43,6 +43,7 @@ public class JpaComplianceTestingImpl implements JpaCompliance {
private boolean listCompliance; private boolean listCompliance;
private boolean closedCompliance; private boolean closedCompliance;
private boolean proxyCompliance; private boolean proxyCompliance;
private boolean cacheCompliance;
@Override @Override
public boolean isJpaQueryComplianceEnabled() { public boolean isJpaQueryComplianceEnabled() {
@ -68,4 +69,9 @@ public class JpaComplianceTestingImpl implements JpaCompliance {
public boolean isJpaProxyComplianceEnabled() { public boolean isJpaProxyComplianceEnabled() {
return proxyCompliance; return proxyCompliance;
} }
@Override
public boolean isJpaCacheComplianceEnabled() {
return cacheCompliance;
}
} }

View File

@ -0,0 +1,211 @@
/*
* 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.jpa.compliance.tck2_2.caching;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.Cacheable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.SecondaryTable;
import javax.persistence.SharedCacheMode;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.Hibernate;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.junit.After;
import org.junit.Test;
import org.hamcrest.CoreMatchers;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hibernate.testing.transaction.TransactionUtil2.inTransaction;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author Steve Ebersole
*/
public class CachingWithSecondaryTablesTests extends BaseUnitTestCase {
private SessionFactoryImplementor sessionFactory;
@Test
public void testUnstrictUnversioned() {
sessionFactory = buildSessionFactory( Person.class, false );
final StatisticsImplementor statistics = sessionFactory.getStatistics();
inTransaction(
sessionFactory,
s -> s.persist( new Person( "1", "John Doe", true ) )
);
// it should not be in the cache because it should be invalidated instead
assertEquals( statistics.getSecondLevelCachePutCount(), 0 );
assertFalse( sessionFactory.getCache().contains( Person.class, "1" ) );
inTransaction(
sessionFactory,
s -> {
statistics.clear();
final Person person = s.get( Person.class, "1" );
assertTrue( Hibernate.isInitialized( person ) );
assertThat( statistics.getSecondLevelCacheHitCount(), CoreMatchers.is( 0L) );
statistics.clear();
}
);
}
@Test
public void testStrictUnversioned() {
sessionFactory = buildSessionFactory( Person.class, true );
final StatisticsImplementor statistics = sessionFactory.getStatistics();
inTransaction(
sessionFactory,
s -> s.persist( new Person( "1", "John Doe", true ) )
);
// this time it should be iun the cache because we enabled JPA compliance
assertEquals( statistics.getSecondLevelCachePutCount(), 1 );
assertTrue( sessionFactory.getCache().contains( Person.class, "1" ) );
inTransaction(
sessionFactory,
s -> {
statistics.clear();
final Person person = s.get( Person.class, "1" );
assertTrue( Hibernate.isInitialized( person ) );
assertThat( statistics.getSecondLevelCacheHitCount(), CoreMatchers.is( 1L) );
statistics.clear();
}
);
}
@Test
public void testVersioned() {
sessionFactory = buildSessionFactory( VersionedPerson.class, false );
final StatisticsImplementor statistics = sessionFactory.getStatistics();
inTransaction(
sessionFactory,
s -> s.persist( new VersionedPerson( "1", "John Doe", true ) )
);
// versioned data should be cacheable regardless
assertEquals( statistics.getSecondLevelCachePutCount(), 1 );
assertTrue( sessionFactory.getCache().contains( VersionedPerson.class, "1" ) );
inTransaction(
sessionFactory,
s -> {
statistics.clear();
final VersionedPerson person = s.get( VersionedPerson.class, "1" );
assertTrue( Hibernate.isInitialized( person ) );
assertThat( statistics.getSecondLevelCacheHitCount(), CoreMatchers.is( 1L ) );
statistics.clear();
}
);
}
private SessionFactoryImplementor buildSessionFactory(Class entityClass, boolean strict) {
final Map settings = new HashMap();
settings.put( AvailableSettings.USE_SECOND_LEVEL_CACHE, "true" );
settings.put( AvailableSettings.JPA_SHARED_CACHE_MODE, SharedCacheMode.ENABLE_SELECTIVE );
settings.put( AvailableSettings.GENERATE_STATISTICS, "true" );
settings.put( AvailableSettings.HBM2DDL_AUTO, "create-drop" );
if ( strict ) {
settings.put( AvailableSettings.JPA_CACHING_COMPLIANCE, "true" );
}
final StandardServiceRegistryBuilder ssrb = new StandardServiceRegistryBuilder()
.applySettings( settings );
return (SessionFactoryImplementor) new MetadataSources( ssrb.build() )
.addAnnotatedClass( Person.class )
.addAnnotatedClass( VersionedPerson.class)
.buildMetadata()
.buildSessionFactory();
}
@After
public void cleanupData() {
if ( sessionFactory == null ) {
return;
}
inTransaction(
sessionFactory,
s -> {
s.createQuery( "delete from Person" ).executeUpdate();
}
);
sessionFactory.close();
}
@Entity( name = "Person" )
@Table( name = "persons" )
@Cacheable()
@SecondaryTable( name = "crm_persons" )
public static class Person {
@Id
public String id;
public String name;
@Column( table = "crm_persons" )
public boolean crmMarketingSchpele;
public Person() {
}
public Person(String id, String name, boolean crmMarketingSchpele) {
this.id = id;
this.name = name;
}
}
@Entity( name = "VersionedPerson" )
@Table( name = "versioned_persons" )
@Cacheable()
@SecondaryTable( name = "crm_persons2" )
public static class VersionedPerson {
@Id
public String id;
public String name;
@Version public int version;
@Column( table = "crm_persons2" )
public boolean crmMarketingSchpele;
public VersionedPerson() {
}
public VersionedPerson(String id, String name, boolean crmMarketingSchpele) {
this.id = id;
this.name = name;
}
}
}