Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.hibernate.event.spi.PreDeleteEventListener;
import org.hibernate.metamodel.mapping.NaturalIdMapping;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.stat.internal.StatsHelper;
import org.hibernate.stat.spi.StatisticsImplementor;

/**
Expand Down Expand Up @@ -297,6 +298,14 @@ protected void removeCacheItem(Object ck) {
final EntityPersister persister = getPersister();
if ( persister.canWriteToCache() ) {
persister.getCacheAccessStrategy().remove( getSession(), ck );

final StatisticsImplementor statistics = getSession().getFactory().getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.entityCacheRemove(
StatsHelper.getRootEntityRole( persister ),
getPersister().getCacheAccessStrategy().getRegion().getName()
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,33 @@
/**
* Specifies an {@code on delete} action for a foreign key constraint.
* The most common usage is {@code @OnDelete(action = CASCADE)}.
* <pre>
* &#064;ManyToOne
* &#064;OnDelete(action = CASCADE)
* Parent parent;
* </pre>
* Note that this results in an {@code on delete cascade} clause in
* the DDL definition of the foreign key. It's completely different
* to {@link jakarta.persistence.CascadeType#REMOVE}.
* <p>
* In fact, {@code @OnDelete} may be combined with {@code cascade=REMOVE}.
* <pre>
* &#064;ManyToOne(cascade = REMOVE)
* &#064;OnDelete(action = CASCADE)
* Parent parent;
* </pre>
* <ul>
* <li>If {@code @OnDelete(action = CASCADE)} is used in conjunction
* with {@code cascade=REMOVE}, then associated entities are fetched
* from the database, marked deleted in the persistence context,
* and evicted from the second-level cache.
* <li>If {@code @OnDelete(action = CASCADE)} is used on its own,
* <em>without</em> {@code cascade=REMOVE}, then associated
* entities are not fetched from the database, are not marked
* deleted in the persistence context, and are not automatically
* evicted from the second-level cache.
* </ul>
* <p>
* Like database triggers, {@code on delete} actions can cause state
* held in memory to lose synchronization with the database.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public interface CacheRegionStatistics extends Serializable {
*/
long getMissCount();

/**
* The number of removals since the last Statistics clearing
*/
long getRemoveCount();

/**
* The number of elements currently in memory within the cache provider.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ public interface CacheableDataStatistics extends Serializable {
* configured cache region since the last Statistics clearing
*/
long getCacheMissCount();

/**
* The number of evictions from the configured cache region since
* the last Statistics clearing
*/
long getCacheRemoveCount();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public abstract class AbstractCacheableDataStatistics implements CacheableDataSt
private final @Nullable LongAdder cacheHitCount;
private final @Nullable LongAdder cacheMissCount;
private final @Nullable LongAdder cachePutCount;
private final @Nullable LongAdder cacheRemoveCount;

public AbstractCacheableDataStatistics(Supplier<@Nullable Region> regionSupplier) {
final Region region = regionSupplier.get();
Expand All @@ -29,12 +30,14 @@ public AbstractCacheableDataStatistics(Supplier<@Nullable Region> regionSupplier
this.cacheHitCount = null;
this.cacheMissCount = null;
this.cachePutCount = null;
this.cacheRemoveCount = null;
}
else {
this.cacheRegionName = region.getName();
this.cacheHitCount = new LongAdder();
this.cacheMissCount = new LongAdder();
this.cachePutCount = new LongAdder();
this.cacheRemoveCount = new LongAdder();
}
}

Expand All @@ -43,6 +46,7 @@ public AbstractCacheableDataStatistics(Supplier<@Nullable Region> regionSupplier
return cacheRegionName;
}

@Override
public long getCacheHitCount() {
if ( cacheRegionName == null ) {
return NOT_CACHED_COUNT;
Expand All @@ -51,6 +55,7 @@ public long getCacheHitCount() {
return NullnessUtil.castNonNull( cacheHitCount ).sum();
}

@Override
public long getCachePutCount() {
if ( cacheRegionName == null ) {
return NOT_CACHED_COUNT;
Expand All @@ -59,6 +64,7 @@ public long getCachePutCount() {
return NullnessUtil.castNonNull( cachePutCount ).sum();
}

@Override
public long getCacheMissCount() {
if ( cacheRegionName == null ) {
return NOT_CACHED_COUNT;
Expand All @@ -67,6 +73,15 @@ public long getCacheMissCount() {
return NullnessUtil.castNonNull( cacheMissCount ).sum();
}

@Override
public long getCacheRemoveCount() {
if ( cacheRegionName == null ) {
return NOT_CACHED_COUNT;
}

return NullnessUtil.castNonNull( cacheRemoveCount ).sum();
}

public void incrementCacheHitCount() {
if ( cacheRegionName == null ) {
throw new IllegalStateException( "Illegal attempt to increment cache hit count for non-cached data" );
Expand All @@ -91,6 +106,14 @@ public void incrementCachePutCount() {
NullnessUtil.castNonNull( cachePutCount ).increment();
}

public void incrementCacheRemoveCount() {
if ( cacheRegionName == null ) {
throw new IllegalStateException( "Illegal attempt to increment cache put count for non-cached data" );
}

NullnessUtil.castNonNull( cacheRemoveCount ).increment();
}

protected void appendCacheStats(StringBuilder buf) {
buf.append( ",cacheRegion=" ).append( cacheRegionName );

Expand All @@ -100,7 +123,8 @@ protected void appendCacheStats(StringBuilder buf) {

buf.append( ",cacheHitCount=" ).append( getCacheHitCount() )
.append( ",cacheMissCount=" ).append( getCacheMissCount() )
.append( ",cachePutCount=" ).append( getCachePutCount() );
.append( ",cachePutCount=" ).append( getCachePutCount() )
.append( ",cacheRemoveCount=" ).append( getCacheRemoveCount() );

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class CacheRegionStatisticsImpl implements CacheRegionStatistics, Seriali
private final LongAdder hitCount = new LongAdder();
private final LongAdder missCount = new LongAdder();
private final LongAdder putCount = new LongAdder();
private final LongAdder removeCount = new LongAdder();

CacheRegionStatisticsImpl(Region region) {
this.region = region;
Expand All @@ -47,6 +48,11 @@ public long getPutCount() {
return putCount.sum();
}

@Override
public long getRemoveCount() {
return removeCount.sum();
}

@Override
public long getElementCountInMemory() {
return region instanceof ExtendedStatisticsSupport extended
Expand Down Expand Up @@ -80,13 +86,19 @@ void incrementPutCount() {
putCount.increment();
}


public void incrementRemoveCount() {
removeCount.increment();
}

@Override
public String toString() {
String buf = "CacheRegionStatistics" +
"[region=" + region.getName() +
",hitCount=" + this.hitCount +
",missCount=" + this.missCount +
",putCount=" + this.putCount +
",removeCount=" + this.removeCount +
",elementCountInMemory=" + this.getElementCountInMemory() +
",elementCountOnDisk=" + this.getElementCountOnDisk() +
",sizeInMemory=" + this.getSizeInMemory() +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,13 @@ public void entityCacheMiss(NavigableRole entityName, String regionName) {
getEntityStatistics( entityName.getFullPath() ).incrementCacheMissCount();
}

@Override
public void entityCacheRemove(NavigableRole entityName, String regionName) {
secondLevelCacheMissCount.increment();
getDomainDataRegionStatistics( regionName ).incrementRemoveCount();
getEntityStatistics( entityName.getFullPath() ).incrementCacheRemoveCount();
}


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Collection stats
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,24 +148,31 @@ public interface StatisticsImplementor extends Statistics, Service {
/**
* Callback indicating a put into second level cache.
*
* @apiNote `entityName` should be the root entity name
* @apiNote {@code entityName} should be the root entity name
*/
void entityCachePut(NavigableRole entityName, String regionName);

/**
* Callback indicating a get from second level cache resulted in a hit.
*
* @apiNote `entityName` should be the root entity name
* @apiNote {@code entityName} should be the root entity name
*/
void entityCacheHit(NavigableRole entityName, String regionName);

/**
* Callback indicating a get from second level cache resulted in a miss.
*
* @apiNote `entityName` should be the root entity name
* @apiNote {@code entityName} should be the root entity name
*/
void entityCacheMiss(NavigableRole entityName, String regionName);

/**
* Callback indicating a removal from second level cache.
*
* @apiNote {@code entityName} should be the root entity name
*/
void entityCacheRemove(NavigableRole rootEntityRole, String name);

/**
* Callback indicating a put into second level cache.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.ondeletecascade;

import jakarta.persistence.Cacheable;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import org.hibernate.Hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.stat.EntityStatistics;
import org.hibernate.stat.Statistics;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.Test;

import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Jpa(annotatedClasses =
{OnDeleteCascadeRemoveTest.Parent.class,
OnDeleteCascadeRemoveTest.Child.class},
generateStatistics = true,
useCollectingStatementInspector = true)
class OnDeleteCascadeRemoveTest {
@Test
void testOnDeleteCascadeRemove1(EntityManagerFactoryScope scope) {
Statistics statistics =
scope.getEntityManagerFactory().unwrap( SessionFactory.class )
.getStatistics();
statistics.clear();
scope.getCollectingStatementInspector().clear();
scope.inTransaction( em -> {
Parent parent = new Parent();
Child child = new Child();
parent.children.add( child );
child.parent = parent;
em.persist( parent );
} );
scope.inTransaction( em -> {
Parent parent = em.find( Parent.class, 0L );
assertFalse( Hibernate.isInitialized( parent.children ) );
em.remove( parent );
// note: ideally we would skip the initialization here
assertTrue( Hibernate.isInitialized( parent.children ) );
});
EntityStatistics entityStatistics = statistics.getEntityStatistics( Child.class.getName() );
assertEquals( 1L, entityStatistics.getDeleteCount() );
assertEquals( 1L, entityStatistics.getCacheRemoveCount() );
assertEquals( 5, scope.getCollectingStatementInspector().getSqlQueries().size() );
long children =
scope.fromTransaction( em -> em.createQuery( "select count(*) from CascadeChild", Long.class )
.getSingleResult() );
assertEquals( 0L, children );
}

@Test
void testOnDeleteCascadeRemove2(EntityManagerFactoryScope scope) {
Statistics statistics =
scope.getEntityManagerFactory().unwrap( SessionFactory.class )
.getStatistics();
statistics.clear();
scope.getCollectingStatementInspector().clear();
scope.inTransaction( em -> {
Parent parent = new Parent();
Child child = new Child();
parent.children.add( child );
child.parent = parent;
em.persist( parent );
} );
scope.inTransaction( em -> {
Parent parent = em.find( Parent.class, 0L );
assertEquals(1, parent.children.size());
assertTrue( Hibernate.isInitialized( parent.children ) );
em.remove( parent );
assertTrue( em.unwrap( SessionImplementor.class )
.getPersistenceContext()
.getEntry( parent.children.iterator().next() )
.getStatus().isDeletedOrGone() );
});
EntityStatistics entityStatistics = statistics.getEntityStatistics( Child.class.getName() );
assertEquals( 1L, entityStatistics.getDeleteCount() );
assertEquals( 1L, entityStatistics.getCacheRemoveCount() );
assertEquals( 5, scope.getCollectingStatementInspector().getSqlQueries().size() );
long children =
scope.fromTransaction( em -> em.createQuery( "select count(*) from CascadeChild", Long.class )
.getSingleResult() );
assertEquals( 0L, children );
}

@Entity(name="CascadeParent")
static class Parent {
@Id
long id;
@OneToMany(mappedBy = "parent",
cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
@OnDelete(action = OnDeleteAction.CASCADE)
Set<Child> children = new HashSet<>();
}
@Entity(name="CascadeChild")
@Cacheable
@SQLDelete( sql = "should never happen" )
static class Child {
@Id
long id;
@OnDelete(action = OnDeleteAction.CASCADE)
@ManyToOne
Parent parent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test;
package org.hibernate.orm.test.ondeletecascade;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test;
package org.hibernate.orm.test.ondeletecascade;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
Expand Down
Loading