diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityAction.java b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityAction.java index 35d2ce984c..90d9b41db5 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityAction.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityAction.java @@ -12,6 +12,7 @@ import org.hibernate.AssertionFailure; import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.action.spi.BeforeTransactionCompletionProcess; import org.hibernate.action.spi.Executable; +import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.service.spi.EventListenerGroup; import org.hibernate.event.service.spi.EventListenerRegistry; @@ -100,7 +101,8 @@ public abstract class EntityAction */ public final Serializable getId() { if ( id instanceof DelayedPostInsertIdentifier ) { - final Serializable eeId = session.getPersistenceContext().getEntry( instance ).getId(); + final EntityEntry entry = session.getPersistenceContext().getEntry( instance ); + final Serializable eeId = entry == null ? null : entry.getId(); return eeId instanceof DelayedPostInsertIdentifier ? null : eeId; } return id; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index a142ab5b7a..9712744e77 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -83,6 +83,7 @@ import static org.hibernate.cfg.AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATE import static org.hibernate.cfg.AvailableSettings.DEFAULT_BATCH_FETCH_SIZE; import static org.hibernate.cfg.AvailableSettings.DEFAULT_ENTITY_MODE; import static org.hibernate.cfg.AvailableSettings.DELAY_ENTITY_LOADER_CREATIONS; +import static org.hibernate.cfg.AvailableSettings.DISABLE_DELAYED_IDENTIFIER_POST_INSERTS; import static org.hibernate.cfg.AvailableSettings.ENABLE_LAZY_LOAD_NO_TRANS; import static org.hibernate.cfg.AvailableSettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH; import static org.hibernate.cfg.AvailableSettings.FLUSH_BEFORE_COMPLETION; @@ -193,7 +194,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { private NullPrecedence defaultNullPrecedence; private boolean orderUpdatesEnabled; private boolean orderInsertsEnabled; - + private boolean postInsertIdentifierDelayed; // multi-tenancy private MultiTenancyStrategy multiTenancyStrategy; @@ -341,6 +342,9 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { this.defaultNullPrecedence = NullPrecedence.parse( defaultNullPrecedence ); this.orderUpdatesEnabled = ConfigurationHelper.getBoolean( ORDER_UPDATES, configurationSettings ); this.orderInsertsEnabled = ConfigurationHelper.getBoolean( ORDER_INSERTS, configurationSettings ); + this.postInsertIdentifierDelayed = !ConfigurationHelper.getBoolean( + DISABLE_DELAYED_IDENTIFIER_POST_INSERTS, configurationSettings, false + ); this.jtaTrackByThread = cfgService.getSetting( JTA_TRACK_BY_THREAD, BOOLEAN, true ); @@ -1044,6 +1048,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { return queryStatisticsMaxSize; } + @Override + public boolean isPostInsertIdentifierDelayableEnabled() { + return postInsertIdentifierDelayed; + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // In-flight mutation access @@ -1175,6 +1184,10 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { this.orderUpdatesEnabled = enabled; } + public void enableDelayedIdentityInserts(boolean enabled) { + this.postInsertIdentifierDelayed = enabled; + } + public void applyMultiTenancyStrategy(MultiTenancyStrategy strategy) { this.multiTenancyStrategy = strategy; } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index a8e0695f47..dd65549056 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -295,4 +295,8 @@ public interface SessionFactoryOptions { default int getQueryStatisticsMaxSize() { return Statistics.DEFAULT_QUERY_STATISTICS_MAX_SIZE; } + + default boolean isPostInsertIdentifierDelayableEnabled() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java index 013124408f..64bdd79fb3 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -1992,4 +1992,19 @@ public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings { * @since 5.4 */ String SEQUENCE_INCREMENT_SIZE_MISMATCH_STRATEGY = "hibernate.id.sequence.increment_size_mismatch_strategy"; + + /** + * This setting controls whether the default behavior for delayed identity inserts is disabled. + *

+ * Hibernate defines a set of rules that are used to determine if an insert statement that has + * an identity-based column can be delayed until flush/commit or if the statement must be + * executed early because access to the identity column is needed. + *

+ * In the event that those defined rules are insufficient for a given mapping scenario, this + * setting can be set to {@code true} to permanently disable delayed identity inserts for all + * transactions as a short-term workaround. + *

+ * The default value is {@code false}. + */ + String DISABLE_DELAYED_IDENTIFIER_POST_INSERTS = "hibernate.id.disable_delayed_identifier_post_inserts"; } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java index 120f6c4e0d..4f99ae5bc8 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java @@ -248,7 +248,7 @@ public abstract class AbstractSaveEventListener Serializable id = key == null ? null : key.getIdentifier(); - boolean shouldDelayIdentityInserts = shouldDelayIdentityInserts( requiresImmediateIdAccess, source ); + boolean shouldDelayIdentityInserts = shouldDelayIdentityInserts( requiresImmediateIdAccess, source, persister ); // Put a placeholder in entries, so we don't recurse back and try to save() the // same object again. QUESTION: should this be done before onSave() is called? @@ -320,14 +320,19 @@ public abstract class AbstractSaveEventListener return id; } - private static boolean shouldDelayIdentityInserts(boolean requiresImmediateIdAccess, EventSource source) { - return shouldDelayIdentityInserts( requiresImmediateIdAccess, isPartOfTransaction( source ), source.getHibernateFlushMode() ); + private static boolean shouldDelayIdentityInserts(boolean requiresImmediateIdAccess, EventSource source, EntityPersister persister) { + return shouldDelayIdentityInserts( requiresImmediateIdAccess, isPartOfTransaction( source ), source.getHibernateFlushMode(), persister ); } private static boolean shouldDelayIdentityInserts( boolean requiresImmediateIdAccess, boolean partOfTransaction, - FlushMode flushMode) { + FlushMode flushMode, + EntityPersister persister) { + if ( !persister.getFactory().getSessionFactoryOptions().isPostInsertIdentifierDelayableEnabled() ) { + return false; + } + if ( requiresImmediateIdAccess ) { // todo : make this configurable? as a way to support this behavior with Session#save etc return false; @@ -336,8 +341,19 @@ public abstract class AbstractSaveEventListener // otherwise we should delay the IDENTITY insertions if either: // 1) we are not part of a transaction // 2) we are in FlushMode MANUAL or COMMIT (not AUTO nor ALWAYS) - return !partOfTransaction || flushMode == MANUAL || flushMode == COMMIT; - + if ( !partOfTransaction || flushMode == MANUAL || flushMode == COMMIT ) { + if ( persister.canIdentityInsertBeDelayed() ) { + return true; + } + LOG.debugf( + "Identity insert for entity [%s] should be delayed; however the persister requested early insert.", + persister.getEntityName() + ); + return false; + } + else { + return false; + } } private static boolean isPartOfTransaction(EventSource source) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 747701e884..3f759cc6f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -76,9 +76,11 @@ import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.ValueInclusion; +import org.hibernate.id.ForeignGenerator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.PostInsertIdentifierGenerator; import org.hibernate.id.PostInsertIdentityPersister; +import org.hibernate.id.enhanced.SequenceStyleGenerator; import org.hibernate.id.insert.Binder; import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; import org.hibernate.internal.CoreLogging; @@ -106,6 +108,7 @@ import org.hibernate.mapping.Table; import org.hibernate.metadata.ClassMetadata; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.persister.walking.internal.EntityIdentifierDefinitionHelper; import org.hibernate.persister.walking.spi.AttributeDefinition; @@ -294,6 +297,8 @@ public abstract class AbstractEntityPersister private final boolean useReferenceCacheEntries; + private boolean canIdentityInsertBeDelayed; + protected void addDiscriminatorToInsert(Insert insert) { } @@ -956,6 +961,11 @@ public abstract class AbstractEntityPersister return useReferenceCacheEntries; } + @Override + public boolean canIdentityInsertBeDelayed() { + return canIdentityInsertBeDelayed; + } + protected static String getTemplateFromString(String string, SessionFactoryImplementor factory) { return string == null ? null : @@ -4148,6 +4158,58 @@ public abstract class AbstractEntityPersister return new SubstituteBracketSQLQueryParser( sql, getFactory() ).process(); } + private void resolveIdentityInsertDelayable() { + // By default they can + // The remainder of this method checks use cases where we shouldn't permit it. + canIdentityInsertBeDelayed = true; + + if ( getEntityMetamodel().getIdentifierProperty().isIdentifierAssignedByInsert() ) { + // if the persister's identifier is assigned by insert, we need to see if we must force non-delay mode. + for ( NonIdentifierAttribute attribute : getEntityMetamodel().getProperties() ) { + if ( isTypeSelfReferencing( attribute.getType() ) ) { + canIdentityInsertBeDelayed = false; + } + } + } + } + + private boolean isTypeSelfReferencing(Type propertyType) { + if ( propertyType.isAssociationType() ) { + if ( propertyType.isEntityType() ) { + Class entityClass = propertyType.getReturnedClass(); + if ( getMappedClass().equals( propertyType.getReturnedClass() ) ) { + return true; + } + } + else if ( propertyType.isCollectionType() ) { + // Association is a collection where owner needs identifier up-front + final CollectionType collection = (CollectionType) propertyType; + final CollectionPersister collectionPersister = getFactory().getMetamodel().collectionPersister( collection.getRole() ); + if ( collectionPersister.isInverse() ) { + final EntityPersister entityPersister = collectionPersister.getOwnerEntityPersister(); + if ( collectionPersister.getOwnerEntityPersister().equals( this ) ) { + final QueryableCollection queryableCollection = (QueryableCollection) collectionPersister; + final IdentifierGenerator identifierGenerator = queryableCollection.getElementPersister().getIdentifierGenerator(); + // todo - perhaps this can be simplified + if ( ( identifierGenerator instanceof ForeignGenerator ) || ( identifierGenerator instanceof SequenceStyleGenerator ) ) { + return true; + } + } + } + } + } + else if ( propertyType.isComponentType() ) { + final CompositeType componentType = (CompositeType) propertyType; + for ( Type componentSubType : componentType.getSubtypes() ) { + if ( isTypeSelfReferencing( componentSubType ) ) { + return true; + } + } + } + + return false; + } + public final void postInstantiate() throws MappingException { doLateInit(); @@ -4155,6 +4217,8 @@ public abstract class AbstractEntityPersister createUniqueKeyLoaders(); createQueryLoader(); + resolveIdentityInsertDelayable(); + doPostInstantiate(); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index eb47a19382..dafe858c46 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -811,4 +811,8 @@ public interface EntityPersister extends EntityDefinition { int[] resolveAttributeIndexes(String[] attributeNames); boolean canUseReferenceCacheEntries(); + + default boolean canIdentityInsertBeDelayed() { + return false; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/flush/TestFlushModeWithIdentitySelfReferenceTest.java b/hibernate-core/src/test/java/org/hibernate/test/flush/TestFlushModeWithIdentitySelfReferenceTest.java index c5d2b0c5e6..c598634a64 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/flush/TestFlushModeWithIdentitySelfReferenceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/flush/TestFlushModeWithIdentitySelfReferenceTest.java @@ -1,18 +1,14 @@ /* - * Copyright 2014 JBoss Inc + * Hibernate, Relational Persistence for Idiomatic Java * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . */ package org.hibernate.test.flush; import javax.persistence.CascadeType; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -40,7 +36,7 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction @Override protected Class[] getAnnotatedClasses() { - return new Class[] { SelfRefEntity.class }; + return new Class[] { SelfRefEntity.class, SelfRefEntityWithEmbeddable.class }; } @Test @@ -49,6 +45,7 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction try { session.setHibernateFlushMode( FlushMode.COMMIT ); loadAndAssert( session, createAndInsertEntity( session ) ); + loadAndInsert( session, createAndInsertEntityEmbeddable( session ) ); } finally { session.close(); @@ -61,6 +58,7 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction try { session.setHibernateFlushMode( FlushMode.MANUAL ); loadAndAssert( session, createAndInsertEntity( session ) ); + loadAndInsert( session, createAndInsertEntityEmbeddable( session ) ); } finally { session.close(); @@ -73,6 +71,7 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction try { session.setHibernateFlushMode( FlushMode.AUTO ); loadAndAssert( session, createAndInsertEntity( session ) ); + loadAndInsert( session, createAndInsertEntityEmbeddable( session ) ); } finally { session.close(); @@ -86,6 +85,7 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction SelfRefEntity entity = new SelfRefEntity(); entity.setSelfRefEntity( entity ); entity.setData( "test" ); + entity = (SelfRefEntity) session.merge( entity ); // only during manual flush do we want to force a flush prior to commit @@ -105,6 +105,37 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction } } + private SelfRefEntityWithEmbeddable createAndInsertEntityEmbeddable(Session session) { + try { + session.getTransaction().begin(); + + SelfRefEntityWithEmbeddable entity = new SelfRefEntityWithEmbeddable(); + entity.setData( "test" ); + + SelfRefEntityInfo info = new SelfRefEntityInfo(); + info.setSeflRefEntityEmbedded( entity ); + + entity.setInfo( info ); + + entity = (SelfRefEntityWithEmbeddable) session.merge( entity ); + + // only during manual flush do we want to force a flush prior to commit + if ( session.getHibernateFlushMode().equals( FlushMode.MANUAL ) ) { + session.flush(); + } + + session.getTransaction().commit(); + + return entity; + } + catch ( Exception e ) { + if ( session.getTransaction().isActive() ) { + session.getTransaction().rollback(); + } + throw e; + } + } + private void loadAndAssert(Session session, SelfRefEntity mergedEntity) { final SelfRefEntity loadedEntity = session.get( SelfRefEntity.class, mergedEntity.getId() ); assertNotNull( "Expected to find the merged entity but did not.", loadedEntity ); @@ -112,6 +143,13 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction assertNotNull( "Expected a non-null self reference", loadedEntity.getSelfRefEntity() ); } + private void loadAndInsert(Session session, SelfRefEntityWithEmbeddable mergedEntity) { + final SelfRefEntityWithEmbeddable loadedEntity = session.get( SelfRefEntityWithEmbeddable.class, mergedEntity.getId() ); + assertNotNull( "Expected to find the merged entity but did not.", loadedEntity ); + assertEquals( "test", loadedEntity.getData() ); + assertNotNull( "Expected a non-null self reference in embeddable", loadedEntity.getInfo().getSeflRefEntityEmbedded() ); + } + @Entity(name = "SelfRefEntity") public static class SelfRefEntity { @Id @@ -145,4 +183,52 @@ public class TestFlushModeWithIdentitySelfReferenceTest extends BaseCoreFunction this.data = data; } } + + @Entity(name = "SelfRefEntityWithEmbeddable") + public static class SelfRefEntityWithEmbeddable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @Embedded + private SelfRefEntityInfo info; + private String data; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public SelfRefEntityInfo getInfo() { + return info; + } + + public void setInfo(SelfRefEntityInfo info) { + this.info = info; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + @Embeddable + public static class SelfRefEntityInfo { + @OneToOne(cascade = CascadeType.ALL, optional = true) + private SelfRefEntityWithEmbeddable seflRefEntityEmbedded; + + public SelfRefEntityWithEmbeddable getSeflRefEntityEmbedded() { + return seflRefEntityEmbedded; + } + + public void setSeflRefEntityEmbedded(SelfRefEntityWithEmbeddable seflRefEntityEmbedded) { + this.seflRefEntityEmbedded = seflRefEntityEmbedded; + } + } }