From 8cfe4126f144a5f590be70f223c05e79ff383b5f Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Tue, 13 Feb 2018 14:28:03 -0600 Subject: [PATCH] HHH-12282 - Allow disabling of invalidation of second-level cache entries for multi-table entities --- .../SessionFactoryOptionsBuilder.java | 8 + .../org/hibernate/cfg/AvailableSettings.java | 6 + .../java/org/hibernate/jpa/JpaCompliance.java | 12 + .../hibernate/jpa/spi/JpaComplianceImpl.java | 19 ++ .../entity/AbstractEntityPersister.java | 49 +++- .../entity/JoinedSubclassEntityPersister.java | 7 +- .../entity/SingleTableEntityPersister.java | 13 +- .../jpa/JpaComplianceTestingImpl.java | 6 + .../CachingWithSecondaryTablesTests.java | 211 ++++++++++++++++++ 9 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/jpa/compliance/tck2_2/caching/CachingWithSecondaryTablesTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index 6ad08e9082..a18dfb55d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -1212,6 +1212,14 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { 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() { this.jpaBootstrap = true; } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java index 6c15cde262..f851ec03a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -1834,6 +1834,12 @@ public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings { */ 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} * is the last value generated or the next value to be used. diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/JpaCompliance.java b/hibernate-core/src/main/java/org/hibernate/jpa/JpaCompliance.java index 931b03569e..50d015c52d 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/JpaCompliance.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/JpaCompliance.java @@ -77,4 +77,16 @@ public interface JpaCompliance { * @return {@code true} indicates to behave in the spec-defined way */ 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(); } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaComplianceImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaComplianceImpl.java index 952848ae4f..99ec148101 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaComplianceImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/JpaComplianceImpl.java @@ -21,6 +21,7 @@ public class JpaComplianceImpl implements JpaCompliance { private boolean listCompliance; private boolean closedCompliance; private boolean proxyCompliance; + private boolean cachingCompliance; @SuppressWarnings("ConstantConditions") @@ -52,6 +53,11 @@ public class JpaComplianceImpl implements JpaCompliance { configurationSettings, jpaByDefault ); + cachingCompliance = ConfigurationHelper.getBoolean( + AvailableSettings.JPA_CACHING_COMPLIANCE, + configurationSettings, + jpaByDefault + ); } @Override @@ -79,6 +85,11 @@ public class JpaComplianceImpl implements JpaCompliance { return proxyCompliance; } + @Override + public boolean isJpaCacheComplianceEnabled() { + return cachingCompliance; + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Mutators @@ -97,4 +108,12 @@ public class JpaComplianceImpl implements JpaCompliance { public void setClosedCompliance(boolean closedCompliance) { this.closedCompliance = closedCompliance; } + + public void setProxyCompliance(boolean proxyCompliance) { + this.proxyCompliance = proxyCompliance; + } + + public void setCachingCompliance(boolean cachingCompliance) { + this.cachingCompliance = cachingCompliance; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index cb82643db8..94715d9a8e 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -95,6 +95,7 @@ import org.hibernate.loader.entity.UniqueEntityLoader; import org.hibernate.mapping.Column; import org.hibernate.mapping.Component; import org.hibernate.mapping.Formula; +import org.hibernate.mapping.Join; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; import org.hibernate.mapping.Selectable; @@ -152,6 +153,7 @@ public abstract class AbstractEntityPersister private final SessionFactoryImplementor factory; private final boolean canReadFromCache; private final boolean canWriteToCache; + private final boolean invalidateCache; private final EntityRegionAccessStrategy cacheAccessStrategy; private final NaturalIdRegionAccessStrategy naturalIdRegionAccessStrategy; private final boolean isLazyPropertiesCacheable; @@ -845,6 +847,50 @@ public abstract class AbstractEntityPersister 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") @@ -1273,8 +1319,7 @@ public abstract class AbstractEntityPersister * item. */ public boolean isCacheInvalidationRequired() { - return hasFormulaProperties() || - ( !isVersioned() && ( entityMetamodel.isDynamicUpdate() || getTableSpan() > 1 ) ); + return invalidateCache; } public boolean isLazyPropertiesCacheable() { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index 40167bdba8..c8b9e626ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -243,7 +243,12 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { while ( joinItr.hasNext() ) { Join join = (Join) joinItr.next(); - isNullableTable[tableIndex++] = join.isOptional(); + isNullableTable[tableIndex++] = join.isOptional() || + creationContext.getSessionFactory() + .getSessionFactoryOptions() + .getJpaCompliance() + .isJpaCacheComplianceEnabled(); + Table table = join.getTable(); final String tableName = determineTableName( table, jdbcEnvironment ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java index a9fdbdcfe8..b602947c5d 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java @@ -178,7 +178,11 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { Join join = (Join) joinIter.next(); qualifiedTableNames[j] = determineTableName( join.getTable(), jdbcEnvironment ); isInverseTable[j] = join.isInverse(); - isNullableTable[j] = join.isOptional(); + isNullableTable[j] = join.isOptional() + || creationContext.getSessionFactory() + .getSessionFactoryOptions() + .getJpaCompliance() + .isJpaCacheComplianceEnabled(); cascadeDeleteEnabled[j] = join.getKey().isCascadeDeleteEnabled() && factory.getDialect().supportsCascadeDelete(); @@ -244,7 +248,12 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { isConcretes.add( persistentClass.isClassOrSuperclassJoin( join ) ); isDeferreds.add( join.isSequentialSelect() ); isInverses.add( join.isInverse() ); - isNullables.add( join.isOptional() ); + isNullables.add( + join.isOptional() || creationContext.getSessionFactory() + .getSessionFactoryOptions() + .getJpaCompliance() + .isJpaCacheComplianceEnabled() + ); isLazies.add( lazyAvailable && join.isLazy() ); if ( join.isSequentialSelect() && !persistentClass.isClassOrSuperclassJoin( join ) ) { hasDeferred = true; diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/JpaComplianceTestingImpl.java b/hibernate-core/src/test/java/org/hibernate/jpa/JpaComplianceTestingImpl.java index 267dc30e49..27663c5403 100644 --- a/hibernate-core/src/test/java/org/hibernate/jpa/JpaComplianceTestingImpl.java +++ b/hibernate-core/src/test/java/org/hibernate/jpa/JpaComplianceTestingImpl.java @@ -43,6 +43,7 @@ public class JpaComplianceTestingImpl implements JpaCompliance { private boolean listCompliance; private boolean closedCompliance; private boolean proxyCompliance; + private boolean cacheCompliance; @Override public boolean isJpaQueryComplianceEnabled() { @@ -68,4 +69,9 @@ public class JpaComplianceTestingImpl implements JpaCompliance { public boolean isJpaProxyComplianceEnabled() { return proxyCompliance; } + + @Override + public boolean isJpaCacheComplianceEnabled() { + return cacheCompliance; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/jpa/compliance/tck2_2/caching/CachingWithSecondaryTablesTests.java b/hibernate-core/src/test/java/org/hibernate/test/jpa/compliance/tck2_2/caching/CachingWithSecondaryTablesTests.java new file mode 100644 index 0000000000..8d2c1da0e2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/jpa/compliance/tck2_2/caching/CachingWithSecondaryTablesTests.java @@ -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; + } + } +}