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" )
SessionFactoryBuilder enableJpaListCompliance(boolean enabled);
/**
* @see JpaCompliance#isJpaCascadeComplianceEnabled()
*
* @see org.hibernate.cfg.AvailableSettings#JPA_CASCADE_COMPLIANCE
*/
SessionFactoryBuilder enableJpaCascadeCompliance(boolean enabled);
/**
* @see JpaCompliance#isJpaClosedComplianceEnabled()
*

View File

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

View File

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

View File

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

View File

@ -62,6 +62,20 @@ public interface JpaComplianceSettings {
*/
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
* {@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" );
final PersistContext context = getContext();
final PersistContext context = getContext( session );
//safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap
for ( Map.Entry<Object,EntityEntry> me : persistenceContext.reentrantSafeEntityEntries() ) {
// for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) {
@ -169,19 +169,25 @@ public abstract class AbstractFlushingEventListener implements JpaBootstrapSensi
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
persistenceContext.incrementCascadeLevel();
try {
Cascade.cascade( getCascadingAction(), CascadePoint.BEFORE_FLUSH, session, persister, object, anything );
Cascade.cascade( getCascadingAction(session), CascadePoint.BEFORE_FLUSH, session, persister, object, anything );
}
finally {
persistenceContext.decrementCascadeLevel();
}
}
protected PersistContext getContext() {
return jpaBootstrap ? PersistContext.create() : null;
protected PersistContext getContext(EventSource session) {
return jpaBootstrap || isJpaCascadeComplianceEnabled( session ) ? PersistContext.create() : null;
}
protected CascadingAction<PersistContext> getCascadingAction() {
return jpaBootstrap ? CascadingActions.PERSIST_ON_FLUSH : CascadingActions.SAVE_UPDATE;
protected CascadingAction<PersistContext> getCascadingAction(EventSource session) {
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 );
}
// 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(
mergedIntegrationSettings != null ? mergedIntegrationSettings : integrationSettings,
providedClassLoader,

View File

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

View File

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

View File

@ -80,6 +80,17 @@ public interface JpaCompliance {
*/
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}
* 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
*/
public interface MutableJpaCompliance extends JpaCompliance {
void setCascadeCompliance(boolean cascadeCompliance);
void setListCompliance(boolean listCompliance);
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;
}
@Override
public boolean isJpaCascadeComplianceEnabled() {
return false;
}
@Override
public boolean isJpaListComplianceEnabled() {
return false;

View File

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