HHH-16218 Natural id cache is extremely slow for entities with compound natural id

This commit is contained in:
Andrea Boriero 2023-03-27 12:00:51 +02:00 committed by Christian Beikov
parent c5897db954
commit 40f22e482f
10 changed files with 371 additions and 158 deletions

View File

@ -69,8 +69,16 @@ public class DefaultCacheKeysFactory implements CacheKeysFactory {
} }
} }
public static Object staticCreateNaturalIdKey(Object naturalIdValues, EntityPersister persister, SharedSessionContractImplementor session) { public static Object staticCreateNaturalIdKey(
return new NaturalIdCacheKey( naturalIdValues, persister, session ); Object naturalIdValues,
EntityPersister persister,
SharedSessionContractImplementor session) {
NaturalIdCacheKey.NaturalIdCacheKeyBuilder builder = new NaturalIdCacheKey.NaturalIdCacheKeyBuilder(
naturalIdValues,
persister,
session
);
return builder.build();
} }
public static Object staticGetEntityId(Object cacheKey) { public static Object staticGetEntityId(Object cacheKey) {

View File

@ -9,9 +9,12 @@ package org.hibernate.cache.internal;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.hibernate.Internal; import org.hibernate.Internal;
import org.hibernate.cache.MutableCacheKeyBuilder;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.ValueHolder; import org.hibernate.internal.util.ValueHolder;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
@ -33,12 +36,51 @@ public class NaturalIdCacheKey implements Serializable {
// "transient" is important here -- NaturalIdCacheKey needs to be Serializable // "transient" is important here -- NaturalIdCacheKey needs to be Serializable
private transient ValueHolder<String> toString; private transient ValueHolder<String> toString;
public NaturalIdCacheKey(Object naturalIdValues, EntityPersister persister, SharedSessionContractImplementor session) { public static class NaturalIdCacheKeyBuilder implements MutableCacheKeyBuilder {
this( naturalIdValues, persister, persister.getRootEntityName(), session );
}
public NaturalIdCacheKey(Object naturalIdValues, EntityPersister persister, String entityName, SharedSessionContractImplementor session) { private final String entityName;
this( persister.getNaturalIdMapping().disassemble( naturalIdValues, session ), entityName, session.getTenantIdentifier(), persister.getNaturalIdMapping().calculateHashCode( naturalIdValues, session ) ); private final String tenantIdentifier;
private final List<Object> values;
private int hashCode;
public NaturalIdCacheKeyBuilder(
Object naturalIdValues,
EntityPersister persister,
String entityName,
SharedSessionContractImplementor session) {
this.entityName = entityName;
this.tenantIdentifier = session.getTenantIdentifier();
values = new ArrayList<>();
persister.getNaturalIdMapping().addToCacheKey( this, naturalIdValues, session );
}
public NaturalIdCacheKeyBuilder(
Object naturalIdValues,
EntityPersister persister,
SharedSessionContractImplementor session) {
this( naturalIdValues, persister, persister.getRootEntityName(), session );
}
@Override
public void addValue(Object value) {
values.add( value );
}
@Override
public void addHashCode(int hashCode) {
this.hashCode = 37 * this.hashCode + hashCode;
}
@Override
public NaturalIdCacheKey build() {
return new NaturalIdCacheKey(
values.toArray( new Object[0] ),
entityName,
tenantIdentifier,
hashCode
);
}
} }
@Internal @Internal

View File

@ -32,9 +32,18 @@ public class SimpleCacheKeysFactory implements CacheKeysFactory {
} }
@Override @Override
public Object createNaturalIdKey(Object naturalIdValues, EntityPersister persister, SharedSessionContractImplementor session) { public Object createNaturalIdKey(
Object naturalIdValues,
EntityPersister persister,
SharedSessionContractImplementor session) {
// natural ids always need to be wrapped // natural ids always need to be wrapped
return new NaturalIdCacheKey(naturalIdValues, persister, null, session); NaturalIdCacheKey.NaturalIdCacheKeyBuilder builder = new NaturalIdCacheKey.NaturalIdCacheKeyBuilder(
naturalIdValues,
persister,
null,
session
);
return builder.build();
} }
@Override @Override

View File

@ -770,7 +770,7 @@ public class NaturalIdResolutionsImpl implements NaturalIdResolutions, Serializa
final int prime = 31; final int prime = 31;
int hashCodeCalculation = 1; int hashCodeCalculation = 1;
hashCodeCalculation = prime * hashCodeCalculation + entityDescriptor.hashCode(); hashCodeCalculation = prime * hashCodeCalculation + entityDescriptor.hashCode();
hashCodeCalculation = prime * hashCodeCalculation + entityDescriptor.getNaturalIdMapping().calculateHashCode( naturalIdValue, persistenceContext.getSession() ); hashCodeCalculation = prime * hashCodeCalculation + entityDescriptor.getNaturalIdMapping().calculateHashCode( naturalIdValue );
this.hashCode = hashCodeCalculation; this.hashCode = hashCodeCalculation;
} }

View File

@ -112,7 +112,7 @@ public interface NaturalIdMapping extends VirtualModelPart {
* @param value The natural-id value * @param value The natural-id value
* @return The hash-code * @return The hash-code
*/ */
int calculateHashCode(Object value, SharedSessionContractImplementor session); int calculateHashCode(Object value);
/** /**
* Make a loader capable of loading a single entity by natural-id * Make a loader capable of loading a single entity by natural-id

View File

@ -172,9 +172,19 @@ public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implement
} }
@Override @Override
public int calculateHashCode(Object value, SharedSessionContractImplementor session) { public int calculateHashCode(Object value) {
Object o = disassemble( value, session ) ; if ( value == null ) {
return Arrays.hashCode((Object[]) o); return 0;
}
Object[] values = (Object[]) value;
int hashcode = 0;
for ( int i = 0; i < attributes.size(); i++ ) {
final Object o = values[i];
if ( o != null ) {
hashcode = 27 * hashcode + ( (JavaType) attributes.get( i ).getExpressibleJavaType() ).extractHashCode( o );
}
}
return hashcode;
} }
@Override @Override

View File

@ -136,7 +136,7 @@ public class SimpleNaturalIdMapping extends AbstractNaturalIdMapping implements
} }
@Override @Override
public int calculateHashCode(Object value, SharedSessionContractImplementor session) { public int calculateHashCode(Object value) {
//noinspection unchecked //noinspection unchecked
return value == null ? 0 : ( (JavaType<Object>) getJavaType() ).extractHashCode( value ); return value == null ? 0 : ( (JavaType<Object>) getJavaType() ).extractHashCode( value );
} }

View File

@ -46,7 +46,7 @@ public class NaturalIdCacheKeyTest {
when( entityPersister.getRootEntityName() ).thenReturn( "EntityName" ); when( entityPersister.getRootEntityName() ).thenReturn( "EntityName" );
when( entityPersister.getNaturalIdMapping() ).thenReturn( naturalIdMapping ); when( entityPersister.getNaturalIdMapping() ).thenReturn( naturalIdMapping );
when( naturalIdMapping.disassemble( any(), eq( sessionImplementor ) ) ).thenAnswer( invocation -> invocation.getArguments()[0] ); when( naturalIdMapping.disassemble( any(), eq( sessionImplementor ) ) ).thenAnswer( invocation -> invocation.getArguments()[0] );
when( naturalIdMapping.calculateHashCode( any(), eq( sessionImplementor ) ) ).thenAnswer( invocation -> invocation.getArguments()[0].hashCode() ); when( naturalIdMapping.calculateHashCode( any() ) ).thenAnswer( invocation -> invocation.getArguments()[0].hashCode() );
final NaturalIdCacheKey key = (NaturalIdCacheKey) DefaultCacheKeysFactory.staticCreateNaturalIdKey( new Object[] {"a", "b", "c"}, entityPersister, sessionImplementor ); final NaturalIdCacheKey key = (NaturalIdCacheKey) DefaultCacheKeysFactory.staticCreateNaturalIdKey( new Object[] {"a", "b", "c"}, entityPersister, sessionImplementor );

View File

@ -6,181 +6,164 @@
*/ */
package org.hibernate.orm.test.mapping.naturalid.compound; package org.hibernate.orm.test.mapping.naturalid.compound;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.NaturalIdCache;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.internal.NaturalIdResolutionsImpl;
import org.hibernate.stat.NaturalIdStatistics;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.annotations.NaturalId;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.TestForIssue;
import org.junit.Test;
import org.junit.jupiter.api.Timeout;
import java.util.function.Function; import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.hibernate.testing.cache.CachingRegionFactory.DEFAULT_ACCESSTYPE;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
* @author Sylvain Dusart * @author Sylvain Dusart
*/ */
@TestForIssue(jiraKey = "HHH-16218") @TestForIssue(jiraKey = "HHH-16218")
public class CompoundNaturalIdCacheTest extends BaseCoreFunctionalTestCase { @DomainModel(
annotatedClasses = {
CompoundNaturalIdCacheTest.EntityWithSimpleNaturalId.class,
CompoundNaturalIdCacheTest.EntityWithCompoundNaturalId.class
}
)
@ServiceRegistry(
settings = {
@Setting(name = DEFAULT_ACCESSTYPE, value = "nonstrict-read-write"),
@Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"),
@Setting(name = AvailableSettings.SHOW_SQL, value = "false"),
}
)
@SessionFactory(generateStatistics = true)
public class CompoundNaturalIdCacheTest {
@Override private static final int OBJECT_NUMBER = 3;
protected Class[] getAnnotatedClasses() {
return new Class[]{
EntityWithSimpleNaturalId.class,
EntityWithCompoundNaturalId.class
};
}
@Override @BeforeAll
protected void configure(Configuration configuration) { public void setUp(SessionFactoryScope scope) {
super.configure(configuration); scope.inTransaction(
session -> {
for ( int i = 0; i < OBJECT_NUMBER; i++ ) {
EntityWithCompoundNaturalId compoundNaturalIdEntity = new EntityWithCompoundNaturalId();
final String str = String.valueOf( i );
compoundNaturalIdEntity.setFirstname( str );
compoundNaturalIdEntity.setLastname( str );
session.persist( compoundNaturalIdEntity );
configuration.setProperty(AvailableSettings.STATEMENT_BATCH_SIZE, "5000"); EntityWithSimpleNaturalId withSimpleNaturalIdEntity = new EntityWithSimpleNaturalId();
configuration.setProperty( AvailableSettings.SHOW_SQL, Boolean.FALSE.toString() ); withSimpleNaturalIdEntity.setName( str );
configuration.setProperty(AvailableSettings.GENERATE_STATISTICS, "false"); session.persist( withSimpleNaturalIdEntity );
} NaturalIdResolutionsImpl naturalIdResolutions = (NaturalIdResolutionsImpl) session.getPersistenceContext()
.getNaturalIdResolutions();
}
}
);
}
@Test @Test
@Timeout(30) public void createThenLoadTest(SessionFactoryScope scope) {
public void createThenLoadTest() { StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
Session s = openSession(); statistics.clear();
Transaction tx = s.beginTransaction(); NaturalIdStatistics naturalIdStatistics = statistics.getNaturalIdStatistics( EntityWithCompoundNaturalId.class.getName() );
int objectsNb = 20000; loadEntityWithCompoundNaturalId( "0", "0", scope );
assertThat( naturalIdStatistics.getCacheHitCount() ).isEqualTo( 1 );
log.info("Starting creations"); loadEntityWithCompoundNaturalId( "1", "1", scope );
assertThat( naturalIdStatistics.getCacheHitCount() ).isEqualTo( 2 );
var creationDurationForCompoundNaturalId = createEntities(i -> { loadEntityWithCompoundNaturalId( "2", "2", scope );
var entity = new EntityWithCompoundNaturalId(); assertThat( naturalIdStatistics.getCacheHitCount() ).isEqualTo( 3 );
final var str = String.valueOf(i);
entity.setFirstname(str);
entity.setLastname(str);
return entity;
}, objectsNb);
log.info("Persisted " + objectsNb + ' ' + EntityWithCompoundNaturalId.class.getSimpleName() + }
" objects, duration=" + creationDurationForCompoundNaturalId + "ms");
var creationDurationForSimpleNaturalId = createEntities(i -> { private void loadEntityWithCompoundNaturalId(String firstname, String lastname, SessionFactoryScope scope) {
var entity = new EntityWithSimpleNaturalId(); scope.inSession(
entity.setName(String.valueOf(i)); session -> {
return entity; session.byNaturalId( EntityWithCompoundNaturalId.class )
}, objectsNb); .using( "firstname", firstname )
.using( "lastname", lastname )
.load();
}
);
}
log.info("Persisted " + objectsNb + ' ' + EntityWithSimpleNaturalId.class.getSimpleName() + @Entity(name = "EntityWithSimpleNaturalId")
" objects, duration=" + creationDurationForSimpleNaturalId + "ms"); @NaturalIdCache
public static class EntityWithSimpleNaturalId {
tx.commit(); @Id
s.close(); @GeneratedValue
private Long id;
int maxResults = 20000; @NaturalId
var loadDurationForCompoundNaturalId = loadEntities(EntityWithCompoundNaturalId.class, maxResults); private String name;
var loadDurationForSimpleNaturalId = loadEntities(EntityWithSimpleNaturalId.class, maxResults);
s.close(); public Long getId() {
return id;
}
assertTrue(loadDurationForCompoundNaturalId <= 5 * loadDurationForSimpleNaturalId, public void setId(final Long id) {
"it should not be soo long to load entities with compound naturalId"); this.id = id;
} }
private long createEntities(final Function<Integer, Object> creator, final int objectsNb) { public String getName() {
var start = System.currentTimeMillis(); return name;
}
for (int i = 0; i < objectsNb; i++) { public void setName(final String name) {
session.persist(creator.apply(i)); this.name = name;
} }
}
return System.currentTimeMillis() - start; @Entity(name = "EntityWithCompoundNaturalId")
} @NaturalIdCache
public static class EntityWithCompoundNaturalId {
private long loadEntities(final Class<?> clazz, final int maxResults) { @Id
var s = openSession(); @GeneratedValue
private Long id;
var start = System.currentTimeMillis(); @NaturalId
log.info("Loading at most " + maxResults + " instances of " + clazz); private String firstname;
final HibernateCriteriaBuilder cb = s.getCriteriaBuilder(); @NaturalId
final var query = cb.createQuery(clazz); private String lastname;
query.from(clazz);
var objects = s.createQuery(query).setMaxResults(maxResults).list();
var duration = System.currentTimeMillis() - start; public Long getId() {
log.info("Loaded " + objects.size() + " instances of " + clazz + ", duration=" + duration + "ms"); return id;
}
s.close(); public void setId(final Long id) {
this.id = id;
}
return duration; public String getFirstname() {
} return firstname;
}
@Entity public void setFirstname(final String firstname) {
public static class EntityWithSimpleNaturalId { this.firstname = firstname;
}
@Id public String getLastname() {
@GeneratedValue return lastname;
private Long id; }
@NaturalId public void setLastname(final String lastname) {
private String name; this.lastname = lastname;
}
public Long getId() { }
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
@Entity
public static class EntityWithCompoundNaturalId {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String firstname;
@NaturalId
private String lastname;
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(final String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(final String lastname) {
this.lastname = lastname;
}
}
} }

View File

@ -0,0 +1,161 @@
/*
* 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.orm.test.mapping.naturalid.compound;
import org.hibernate.annotations.NaturalId;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.NaturalIdResolutions;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaCriteriaQuery;
import org.hibernate.query.criteria.JpaRoot;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author Sylvain Dusart
*/
@TestForIssue(jiraKey = "HHH-16218")
@DomainModel(
annotatedClasses = {
CompoundNaturalIdTest.EntityWithSimpleNaturalId.class,
CompoundNaturalIdTest.EntityWithCompoundNaturalId.class
}
)
@ServiceRegistry(
settings = {
@Setting(name = AvailableSettings.STATEMENT_BATCH_SIZE, value = "500"),
@Setting(name = AvailableSettings.SHOW_SQL, value = "false"),
}
)
@SessionFactory
public class CompoundNaturalIdTest {
private static final int OBJECT_NUMBER = 2000;
private static final int MAX_RESULTS = 2000;
@BeforeAll
public void setUp(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
for ( int i = 0; i < OBJECT_NUMBER; i++ ) {
EntityWithCompoundNaturalId compoundNaturalIdEntity = new EntityWithCompoundNaturalId();
final String str = String.valueOf( i );
compoundNaturalIdEntity.setFirstname( str );
compoundNaturalIdEntity.setLastname( str );
session.persist( compoundNaturalIdEntity );
EntityWithSimpleNaturalId withSimpleNaturalIdEntity = new EntityWithSimpleNaturalId();
withSimpleNaturalIdEntity.setName( str );
session.persist( withSimpleNaturalIdEntity );
}
}
);
}
@Test
public void createThenLoadTest(SessionFactoryScope scope) {
long loadDurationForCompoundNaturalId = loadEntities( EntityWithCompoundNaturalId.class, MAX_RESULTS, scope );
long loadDurationForSimpleNaturalId = loadEntities( EntityWithSimpleNaturalId.class, MAX_RESULTS, scope );
assertTrue(
loadDurationForCompoundNaturalId <= 5 * loadDurationForSimpleNaturalId,
"it should not be soo long to load entities with compound naturalId"
);
}
private long loadEntities(final Class<?> clazz, final int maxResults, SessionFactoryScope scope) {
long start = System.currentTimeMillis();
scope.inSession(
session -> {
final HibernateCriteriaBuilder cb = session.getCriteriaBuilder();
JpaCriteriaQuery<?> query = cb.createQuery( clazz );
query.from( clazz );
session.createQuery( query ).setMaxResults( maxResults ).list();
}
);
long duration = System.currentTimeMillis() - start;
return duration;
}
@Entity(name = "EntityWithSimpleNaturalId")
public static class EntityWithSimpleNaturalId {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
@Entity(name = "EntityWithCompoundNaturalId")
public static class EntityWithCompoundNaturalId {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String firstname;
@NaturalId
private String lastname;
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(final String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(final String lastname) {
this.lastname = lastname;
}
}
}