HHH-12282 - Allow disabling of invalidation of second-level cache entries for multi-table entities
This commit is contained in:
parent
5e397e9cb3
commit
8cfe4126f1
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue