HHH-18166 introduce hibernate.jpa.compliance.cascade

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-05-25 22:41:45 +02:00
parent ffab0d8026
commit e3cf006e76
15 changed files with 237 additions and 13 deletions

View File

@ -731,6 +731,13 @@ public interface SessionFactoryBuilder {
@Deprecated( since = "6.0" ) @Deprecated( since = "6.0" )
SessionFactoryBuilder enableJpaListCompliance(boolean enabled); SessionFactoryBuilder enableJpaListCompliance(boolean enabled);
/**
* @see JpaCompliance#isJpaCascadeComplianceEnabled()
*
* @see org.hibernate.cfg.AvailableSettings#JPA_CASCADE_COMPLIANCE
*/
SessionFactoryBuilder enableJpaCascadeCompliance(boolean enabled);
/** /**
* @see JpaCompliance#isJpaClosedComplianceEnabled() * @see JpaCompliance#isJpaClosedComplianceEnabled()
* *

View File

@ -424,6 +424,12 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
return this; return this;
} }
@Override
public SessionFactoryBuilder enableJpaCascadeCompliance(boolean enabled) {
this.optionsBuilder.enableJpaCascadeCompliance( enabled );
return this;
}
@Override @Override
public SessionFactoryBuilder enableJpaClosedCompliance(boolean enabled) { public SessionFactoryBuilder enableJpaClosedCompliance(boolean enabled) {
this.optionsBuilder.enableJpaClosedCompliance( enabled ); this.optionsBuilder.enableJpaClosedCompliance( enabled );

View File

@ -1565,6 +1565,10 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
mutableJpaCompliance().setListCompliance( enabled ); mutableJpaCompliance().setListCompliance( enabled );
} }
public void enableJpaCascadeCompliance(boolean enabled) {
mutableJpaCompliance().setCascadeCompliance( enabled );
}
public void enableJpaClosedCompliance(boolean enabled) { public void enableJpaClosedCompliance(boolean enabled) {
mutableJpaCompliance().setClosedCompliance( enabled ); mutableJpaCompliance().setClosedCompliance( enabled );
} }

View File

@ -374,6 +374,12 @@ public abstract class AbstractDelegatingSessionFactoryBuilder<T extends SessionF
return getThis(); return getThis();
} }
@Override
public SessionFactoryBuilder enableJpaCascadeCompliance(boolean enabled) {
delegate.enableJpaCascadeCompliance( enabled );
return getThis();
}
@Override @Override
public SessionFactoryBuilder enableJpaListCompliance(boolean enabled) { public SessionFactoryBuilder enableJpaListCompliance(boolean enabled) {
delegate.enableJpaListCompliance( enabled ); delegate.enableJpaListCompliance( enabled );

View File

@ -62,6 +62,20 @@ public interface JpaComplianceSettings {
*/ */
String JPA_QUERY_COMPLIANCE = "hibernate.jpa.compliance.query"; String JPA_QUERY_COMPLIANCE = "hibernate.jpa.compliance.query";
/**
* Controls whether Hibernate applies cascade
* {@link jakarta.persistence.CascadeType#PERSIST} or
* {@link org.hibernate.annotations.CascadeType#SAVE_UPDATE}
* at flush time. If enabled, Hibernate will cascade the standard
* JPA {@code PERSIST} operation. Otherwise, Hibernate will cascade
* the legacy {@code SAVE_UPDATE} operation.
*
* @settingDefault {@link #JPA_COMPLIANCE}
*
* @since 6.6
*/
String JPA_CASCADE_COMPLIANCE = "hibernate.jpa.compliance.cascade";
/** /**
* Controls whether Hibernate should treat what it would usually consider a * Controls whether Hibernate should treat what it would usually consider a
* {@linkplain org.hibernate.collection.spi.PersistentBag "bag"}, that is, a * {@linkplain org.hibernate.collection.spi.PersistentBag "bag"}, that is, a

View File

@ -146,7 +146,7 @@ public abstract class AbstractFlushingEventListener implements JpaBootstrapSensi
LOG.debug( "Processing flush-time cascades" ); LOG.debug( "Processing flush-time cascades" );
final PersistContext context = getContext(); final PersistContext context = getContext( session );
//safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap //safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap
for ( Map.Entry<Object,EntityEntry> me : persistenceContext.reentrantSafeEntityEntries() ) { for ( Map.Entry<Object,EntityEntry> me : persistenceContext.reentrantSafeEntityEntries() ) {
// for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) { // for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) {
@ -169,19 +169,25 @@ public abstract class AbstractFlushingEventListener implements JpaBootstrapSensi
final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
persistenceContext.incrementCascadeLevel(); persistenceContext.incrementCascadeLevel();
try { try {
Cascade.cascade( getCascadingAction(), CascadePoint.BEFORE_FLUSH, session, persister, object, anything ); Cascade.cascade( getCascadingAction(session), CascadePoint.BEFORE_FLUSH, session, persister, object, anything );
} }
finally { finally {
persistenceContext.decrementCascadeLevel(); persistenceContext.decrementCascadeLevel();
} }
} }
protected PersistContext getContext() { protected PersistContext getContext(EventSource session) {
return jpaBootstrap ? PersistContext.create() : null; return jpaBootstrap || isJpaCascadeComplianceEnabled( session ) ? PersistContext.create() : null;
} }
protected CascadingAction<PersistContext> getCascadingAction() { protected CascadingAction<PersistContext> getCascadingAction(EventSource session) {
return jpaBootstrap ? CascadingActions.PERSIST_ON_FLUSH : CascadingActions.SAVE_UPDATE; return jpaBootstrap || isJpaCascadeComplianceEnabled( session )
? CascadingActions.PERSIST_ON_FLUSH
: CascadingActions.SAVE_UPDATE;
}
private static boolean isJpaCascadeComplianceEnabled(EventSource session) {
return session.getSessionFactory().getSessionFactoryOptions().getJpaCompliance().isJpaCascadeComplianceEnabled();
} }
/** /**

View File

@ -245,7 +245,7 @@ public class EntityManagerFactoryBuilderImpl implements EntityManagerFactoryBuil
mergedIntegrationSettings.putAll( integrationSettings ); mergedIntegrationSettings.putAll( integrationSettings );
} }
// Build the boot-strap service registry, which mainly handles class loader interactions // Build the bootstrap service registry, which mainly handles classloader interactions
final BootstrapServiceRegistry bsr = buildBootstrapServiceRegistry( final BootstrapServiceRegistry bsr = buildBootstrapServiceRegistry(
mergedIntegrationSettings != null ? mergedIntegrationSettings : integrationSettings, mergedIntegrationSettings != null ? mergedIntegrationSettings : integrationSettings,
providedClassLoader, providedClassLoader,

View File

@ -21,6 +21,7 @@ public class JpaComplianceImpl implements JpaCompliance {
private final boolean closedCompliance; private final boolean closedCompliance;
private final boolean cachingCompliance; private final boolean cachingCompliance;
private final boolean loadByIdCompliance; private final boolean loadByIdCompliance;
private final boolean cascadeCompliance;
public JpaComplianceImpl( public JpaComplianceImpl(
boolean listCompliance, boolean listCompliance,
@ -31,7 +32,8 @@ public class JpaComplianceImpl implements JpaCompliance {
boolean transactionCompliance, boolean transactionCompliance,
boolean closedCompliance, boolean closedCompliance,
boolean cachingCompliance, boolean cachingCompliance,
boolean loadByIdCompliance) { boolean loadByIdCompliance,
boolean cascadeCompliance) {
this.queryCompliance = queryCompliance; this.queryCompliance = queryCompliance;
this.transactionCompliance = transactionCompliance; this.transactionCompliance = transactionCompliance;
this.listCompliance = listCompliance; this.listCompliance = listCompliance;
@ -41,6 +43,7 @@ public class JpaComplianceImpl implements JpaCompliance {
this.globalGeneratorNameScopeCompliance = globalGeneratorNameScopeCompliance; this.globalGeneratorNameScopeCompliance = globalGeneratorNameScopeCompliance;
this.orderByMappingCompliance = orderByMappingCompliance; this.orderByMappingCompliance = orderByMappingCompliance;
this.loadByIdCompliance = loadByIdCompliance; this.loadByIdCompliance = loadByIdCompliance;
this.cascadeCompliance = cascadeCompliance;
} }
@Override @Override
@ -53,6 +56,11 @@ public class JpaComplianceImpl implements JpaCompliance {
return transactionCompliance; return transactionCompliance;
} }
@Override
public boolean isJpaCascadeComplianceEnabled() {
return cascadeCompliance;
}
@Override @Override
public boolean isJpaListComplianceEnabled() { public boolean isJpaListComplianceEnabled() {
return listCompliance; return listCompliance;
@ -98,10 +106,16 @@ public class JpaComplianceImpl implements JpaCompliance {
private boolean transactionCompliance; private boolean transactionCompliance;
private boolean closedCompliance; private boolean closedCompliance;
private boolean loadByIdCompliance; private boolean loadByIdCompliance;
private boolean cascadeCompliance;
public JpaComplianceBuilder() { public JpaComplianceBuilder() {
} }
public JpaComplianceBuilder setCascadeCompliance(boolean cascadeCompliance) {
this.cascadeCompliance = cascadeCompliance;
return this;
}
public JpaComplianceBuilder setListCompliance(boolean listCompliance) { public JpaComplianceBuilder setListCompliance(boolean listCompliance) {
this.listCompliance = listCompliance; this.listCompliance = listCompliance;
return this; return this;
@ -157,7 +171,8 @@ public class JpaComplianceImpl implements JpaCompliance {
transactionCompliance, transactionCompliance,
closedCompliance, closedCompliance,
cachingCompliance, cachingCompliance,
loadByIdCompliance loadByIdCompliance,
cascadeCompliance
); );
} }
} }

View File

@ -26,6 +26,7 @@ public class MutableJpaComplianceImpl implements MutableJpaCompliance {
private boolean closedCompliance; private boolean closedCompliance;
private boolean cachingCompliance; private boolean cachingCompliance;
private boolean loadByIdCompliance; private boolean loadByIdCompliance;
private boolean cascadeCompliance;
public MutableJpaComplianceImpl(Map<?,?> configurationSettings) { public MutableJpaComplianceImpl(Map<?,?> configurationSettings) {
this( this(
@ -38,6 +39,11 @@ public class MutableJpaComplianceImpl implements MutableJpaCompliance {
public MutableJpaComplianceImpl(Map<?,?> configurationSettings, boolean jpaByDefault) { public MutableJpaComplianceImpl(Map<?,?> configurationSettings, boolean jpaByDefault) {
final Object legacyQueryCompliance = configurationSettings.get( AvailableSettings.JPAQL_STRICT_COMPLIANCE ); final Object legacyQueryCompliance = configurationSettings.get( AvailableSettings.JPAQL_STRICT_COMPLIANCE );
cascadeCompliance = ConfigurationHelper.getBoolean(
AvailableSettings.JPA_CASCADE_COMPLIANCE,
configurationSettings,
jpaByDefault
);
//noinspection deprecation //noinspection deprecation
listCompliance = ConfigurationHelper.getBoolean( listCompliance = ConfigurationHelper.getBoolean(
AvailableSettings.JPA_LIST_COMPLIANCE, AvailableSettings.JPA_LIST_COMPLIANCE,
@ -96,6 +102,10 @@ public class MutableJpaComplianceImpl implements MutableJpaCompliance {
return transactionCompliance; return transactionCompliance;
} }
public boolean isJpaCascadeComplianceEnabled() {
return cascadeCompliance;
}
@Override @Override
public boolean isJpaListComplianceEnabled() { public boolean isJpaListComplianceEnabled() {
return listCompliance; return listCompliance;
@ -139,6 +149,11 @@ public class MutableJpaComplianceImpl implements MutableJpaCompliance {
this.listCompliance = listCompliance; this.listCompliance = listCompliance;
} }
@Override
public void setCascadeCompliance(boolean cascadeCompliance) {
this.cascadeCompliance = cascadeCompliance;
}
@Override @Override
public void setOrderByMappingCompliance(boolean orderByMappingCompliance) { public void setOrderByMappingCompliance(boolean orderByMappingCompliance) {
this.orderByMappingCompliance = orderByMappingCompliance; this.orderByMappingCompliance = orderByMappingCompliance;
@ -182,6 +197,7 @@ public class MutableJpaComplianceImpl implements MutableJpaCompliance {
public JpaCompliance immutableCopy() { public JpaCompliance immutableCopy() {
JpaComplianceImpl.JpaComplianceBuilder builder = new JpaComplianceImpl.JpaComplianceBuilder(); JpaComplianceImpl.JpaComplianceBuilder builder = new JpaComplianceImpl.JpaComplianceBuilder();
builder = builder.setListCompliance( listCompliance ) builder = builder.setListCompliance( listCompliance )
.setCascadeCompliance( cascadeCompliance )
.setProxyCompliance( proxyCompliance ) .setProxyCompliance( proxyCompliance )
.setOrderByMappingCompliance( orderByMappingCompliance ) .setOrderByMappingCompliance( orderByMappingCompliance )
.setGlobalGeneratorNameCompliance( generatorNameScopeCompliance ) .setGlobalGeneratorNameCompliance( generatorNameScopeCompliance )

View File

@ -80,6 +80,17 @@ public interface JpaCompliance {
*/ */
boolean isJpaClosedComplianceEnabled(); boolean isJpaClosedComplianceEnabled();
/**
* JPA specifies that a {@link jakarta.persistence.CascadeType#PERSIST}
* operation should occur at flush time. The legacy behavior of Hibernate
* was a {@link org.hibernate.annotations.CascadeType#SAVE_UPDATE}.
*
* @return {@code true} indicates to behave in the spec-defined way
*
* @see org.hibernate.cfg.AvailableSettings#JPA_CASCADE_COMPLIANCE
*/
boolean isJpaCascadeComplianceEnabled();
/** /**
* JPA spec says that an {@link jakarta.persistence.EntityNotFoundException} * JPA spec says that an {@link jakarta.persistence.EntityNotFoundException}
* should be thrown when accessing an entity proxy which does not have * should be thrown when accessing an entity proxy which does not have

View File

@ -10,6 +10,8 @@ package org.hibernate.jpa.spi;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public interface MutableJpaCompliance extends JpaCompliance { public interface MutableJpaCompliance extends JpaCompliance {
void setCascadeCompliance(boolean cascadeCompliance);
void setListCompliance(boolean listCompliance); void setListCompliance(boolean listCompliance);
void setOrderByMappingCompliance(boolean orderByCompliance); void setOrderByMappingCompliance(boolean orderByCompliance);

View File

@ -0,0 +1,63 @@
package org.hibernate.orm.test.annotations.cascade.persist;
import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
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;
@Jpa(annotatedClasses =
{CascadePersistJpaTest.Parent.class,
CascadePersistJpaTest.Child.class},
jpaComplianceEnabled = true)
public class CascadePersistJpaTest {
@Test void test(EntityManagerFactoryScope scope) {
Parent p = new Parent();
scope.inTransaction(s -> s.persist(p));
scope.inTransaction(s -> {
Parent parent = s.find(Parent.class, p.id);
Child child = new Child();
child.parent = parent;
parent.children.add(child);
});
scope.inTransaction(s -> {
Parent parent = s.find(Parent.class, p.id);
assertEquals(1, parent.children.size());
Child child = parent.children.iterator().next();
s.remove(child);
parent.children.remove(child);
});
}
@Entity
static class Parent {
@Id
@GeneratedValue
Long id;
@OneToMany(cascade = CascadeType.PERSIST,
mappedBy = "parent")
Set<Child> children = new HashSet<>();
}
@Entity
static class Child {
@Id @GeneratedValue
private Long id;
@Basic(optional = false)
String name = "child";
@ManyToOne(optional = false)
Parent parent;
}
}

View File

@ -0,0 +1,68 @@
package org.hibernate.orm.test.annotations.cascade.persist;
import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import org.hibernate.cfg.JpaComplianceSettings;
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.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SessionFactory
@DomainModel(annotatedClasses =
{CascadePersistSFTest.Parent.class,
CascadePersistSFTest.Child.class})
@ServiceRegistry(settings = @Setting(name=JpaComplianceSettings.JPA_CASCADE_COMPLIANCE,value="true"))
public class CascadePersistSFTest {
@Test void test(SessionFactoryScope scope) {
Parent p = new Parent();
scope.inTransaction(s -> s.persist(p));
scope.inTransaction(s -> {
Parent parent = s.find(Parent.class, p.id);
Child child = new Child();
child.parent = parent;
parent.children.add(child);
});
scope.inTransaction(s -> {
Parent parent = s.find(Parent.class, p.id);
assertEquals(1, parent.children.size());
Child child = parent.children.iterator().next();
s.remove(child);
parent.children.remove(child);
});
}
@Entity
static class Parent {
@Id
@GeneratedValue
Long id;
@OneToMany(cascade = CascadeType.PERSIST,
mappedBy = "parent")
Set<Child> children = new HashSet<>();
}
@Entity
static class Child {
@Id @GeneratedValue
private Long id;
@Basic(optional = false)
String name = "child";
@ManyToOne(optional = false)
Parent parent;
}
}

View File

@ -24,6 +24,11 @@ public class JpaComplianceStub implements JpaCompliance {
return false; return false;
} }
@Override
public boolean isJpaCascadeComplianceEnabled() {
return false;
}
@Override @Override
public boolean isJpaListComplianceEnabled() { public boolean isJpaListComplianceEnabled() {
return false; return false;

View File

@ -23,6 +23,7 @@ import org.hibernate.event.internal.DefaultFlushEventListener;
import org.hibernate.event.internal.DefaultPersistEventListener; import org.hibernate.event.internal.DefaultPersistEventListener;
import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.AutoFlushEventListener; import org.hibernate.event.spi.AutoFlushEventListener;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EventType; import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.FlushEntityEventListener; import org.hibernate.event.spi.FlushEntityEventListener;
import org.hibernate.event.spi.FlushEventListener; import org.hibernate.event.spi.FlushEventListener;
@ -142,12 +143,12 @@ public abstract class AbstractJPATest extends BaseSessionFactoryFunctionalTest {
public static final AutoFlushEventListener INSTANCE = new JPAAutoFlushEventListener(); public static final AutoFlushEventListener INSTANCE = new JPAAutoFlushEventListener();
@Override @Override
protected CascadingAction<PersistContext> getCascadingAction() { protected CascadingAction<PersistContext> getCascadingAction(EventSource session) {
return CascadingActions.PERSIST_ON_FLUSH; return CascadingActions.PERSIST_ON_FLUSH;
} }
@Override @Override
protected PersistContext getContext() { protected PersistContext getContext(EventSource session) {
return PersistContext.create(); return PersistContext.create();
} }
} }
@ -157,12 +158,12 @@ public abstract class AbstractJPATest extends BaseSessionFactoryFunctionalTest {
public static final FlushEventListener INSTANCE = new JPAFlushEventListener(); public static final FlushEventListener INSTANCE = new JPAFlushEventListener();
@Override @Override
protected CascadingAction<PersistContext> getCascadingAction() { protected CascadingAction<PersistContext> getCascadingAction(EventSource session) {
return CascadingActions.PERSIST_ON_FLUSH; return CascadingActions.PERSIST_ON_FLUSH;
} }
@Override @Override
protected PersistContext getContext() { protected PersistContext getContext(EventSource session) {
return PersistContext.create(); return PersistContext.create();
} }
} }