diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java index f218ed25d4..963a697ba3 100644 --- a/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java @@ -10,11 +10,16 @@ import java.util.Optional; /** * Loads an entity by its natural identifier. - * + * + * This is a generic form of load-by-natural-id covering both a single attribute + * and multiple attributes as the natural-id. For natural-ids defined by a single + * attribute, {@link SimpleNaturalIdLoadAccess} offers simplified access. + * * @author Eric Dalquist * @author Steve Ebersole * * @see org.hibernate.annotations.NaturalId + * @see Session#byNaturalId */ public interface NaturalIdLoadAccess { /** @@ -36,6 +41,15 @@ public interface NaturalIdLoadAccess { */ NaturalIdLoadAccess using(String attributeName, Object value); + /** + * Set multiple natural-id attribute values at once. The passed array is + * expected to have an even number of elements, with the attribute name followed + * by its value. E.g. `using( "system", "matrix", "username", "neo" )` + * + * @return {@code this}, for method chaining + */ + NaturalIdLoadAccess using(Object... mappings); + /** * For entities with mutable natural ids, should Hibernate perform "synchronization" prior to performing * lookups? The default is to perform "synchronization" (for correctness). diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java new file mode 100644 index 0000000000..58549cb02b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java @@ -0,0 +1,130 @@ +/* + * 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; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.RootGraph; +import org.hibernate.internal.util.collections.CollectionHelper; + +/** + * Defines the ability to load multiple entities by simple natural-id simultaneously. + */ +public interface NaturalIdMultiLoadAccess { + /** + * Specify the {@link LockOptions} to use when retrieving the entity. + * + * @param lockOptions The lock options to use. + * + * @return {@code this}, for method chaining + */ + NaturalIdMultiLoadAccess with(LockOptions lockOptions); + + /** + * Specify the {@link CacheMode} to use when retrieving the entity. + * + * @param cacheMode The CacheMode to use. + * + * @return {@code this}, for method chaining + */ + NaturalIdMultiLoadAccess with(CacheMode cacheMode); + + /** + * Define a load graph to be used when retrieving the entity + */ + default NaturalIdMultiLoadAccess with(RootGraph graph) { + return with( graph, GraphSemantic.LOAD ); + } + + /** + * Define a load or fetch graph to be used when retrieving the entity + */ + NaturalIdMultiLoadAccess with(RootGraph graph, GraphSemantic semantic); + + /** + * Specify a batch size for loading the entities (how many at a time). The default is + * to use a batch sizing strategy defined by the Dialect in use. Any greater-than-one + * value here will override that default behavior. If giving an explicit value here, + * care should be taken to not exceed the capabilities of the underlying database. + *

+ * Note that overall a batch-size is considered a hint. + * + * @param batchSize The batch size + * + * @return {@code this}, for method chaining + */ + NaturalIdMultiLoadAccess withBatchSize(int batchSize); + + /** + * Should the multi-load operation be allowed to return entities that are locally + * deleted? A locally deleted entity is one which has been passed to this + * Session's {@link Session#delete} / {@link Session#remove} method, but not + * yet flushed. The default behavior is to handle them as null in the return + * (see {@link #enableOrderedReturn}). + * + * @param enabled {@code true} enables returning the deleted entities; + * {@code false} (the default) disables it. + * + * @return {@code this}, for method chaining + */ + NaturalIdMultiLoadAccess enableReturnOfDeletedEntities(boolean enabled); + + /** + * Should the return List be ordered and positional in relation to the + * incoming ids? If enabled (the default), the return List is ordered and + * positional relative to the incoming ids. In other words, a request to + * {@code multiLoad([2,1,3])} will return {@code [Entity#2, Entity#1, Entity#3]}. + *

+ * An important distinction is made here in regards to the handling of + * unknown entities depending on this "ordered return" setting. If enabled + * a null is inserted into the List at the proper position(s). If disabled, + * the nulls are not put into the return List. In other words, consumers of + * the returned ordered List would need to be able to handle null elements. + * + * @param enabled {@code true} (the default) enables ordering; + * {@code false} disables it. + * + * @return {@code this}, for method chaining + */ + NaturalIdMultiLoadAccess enableOrderedReturn(boolean enabled); + + /** + * Perform a load of multiple entities by natural-id. + * + * See {@link #enableOrderedReturn} and {@link #enableReturnOfDeletedEntities} + * for options which effect the size and "shape" of the return list. + * + * @param ids The natural-id values to load + * + * @return The managed entities. + */ + List multiLoad(Object... ids); + + /** + * Perform a load of multiple entities by natural-id. + * + * See {@link #enableOrderedReturn} and {@link #enableReturnOfDeletedEntities} + * for options which effect the size and "shape" of the return list. + * + * @param ids The natural-id values to load + * + * @return The managed entities. + */ + List multiLoad(List ids); + + /** + * Helper for creating a Map that represents the value of a compound natural-id + * for use in loading. The passed array is expected to have an even number of elements + * representing key, value pairs. E.g. `using( "system", "matrix", "username", "neo" )` + */ + static Map compoundValue(Object... elements) { + return CollectionHelper.asMap( elements ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 2201494b93..9dee4d1e29 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -784,7 +784,7 @@ public interface Session extends SharedSessionContract, EntityManager, AutoClose * * @throws HibernateException If the specified entity name cannot be resolved as an entity name */ - IdentifierLoadAccess byId(String entityName); + IdentifierLoadAccess byId(String entityName); /** * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple entities at once @@ -808,7 +808,7 @@ public interface Session extends SharedSessionContract, EntityManager, AutoClose * * @throws HibernateException If the specified entity name cannot be resolved as an entity name */ - MultiIdentifierLoadAccess byMultipleIds(String entityName); + MultiIdentifierLoadAccess byMultipleIds(String entityName); /** * Create an {@link IdentifierLoadAccess} instance to retrieve the specified entity by @@ -832,7 +832,7 @@ public interface Session extends SharedSessionContract, EntityManager, AutoClose * * @throws HibernateException If the specified entity name cannot be resolved as an entity name */ - NaturalIdLoadAccess byNaturalId(String entityName); + NaturalIdLoadAccess byNaturalId(String entityName); /** * Create a {@link NaturalIdLoadAccess} instance to retrieve the specified entity by @@ -857,7 +857,7 @@ public interface Session extends SharedSessionContract, EntityManager, AutoClose * @throws HibernateException If the specified entityClass cannot be resolved as a mapped entity, or if the * entity does not define a natural-id or if its natural-id is made up of multiple attributes. */ - SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName); + SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName); /** * Create a {@link SimpleNaturalIdLoadAccess} instance to retrieve the specified entity by @@ -872,6 +872,16 @@ public interface Session extends SharedSessionContract, EntityManager, AutoClose */ SimpleNaturalIdLoadAccess bySimpleNaturalId(Class entityClass); + /** + * Access to load multiple entities by natural-id + */ + NaturalIdMultiLoadAccess byMultipleNaturalId(Class entityClass); + + /** + * Access to load multiple entities by natural-id + */ + NaturalIdMultiLoadAccess byMultipleNaturalId(String entityName); + /** * Enable the named filter for this current session. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index 160e10aa02..1d48e7edb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -39,6 +39,7 @@ import org.hibernate.Session; import org.hibernate.SessionEventListener; import org.hibernate.SharedSessionBuilder; import org.hibernate.SimpleNaturalIdLoadAccess; +import org.hibernate.NaturalIdMultiLoadAccess; import org.hibernate.Transaction; import org.hibernate.UnknownProfileException; import org.hibernate.cache.spi.CacheTransactionSynchronization; @@ -911,7 +912,7 @@ public class SessionDelegatorBaseImpl implements SessionImplementor { } @Override - public IdentifierLoadAccess byId(String entityName) { + public IdentifierLoadAccess byId(String entityName) { return delegate.byId( entityName ); } @@ -921,7 +922,7 @@ public class SessionDelegatorBaseImpl implements SessionImplementor { } @Override - public MultiIdentifierLoadAccess byMultipleIds(String entityName) { + public MultiIdentifierLoadAccess byMultipleIds(String entityName) { return delegate.byMultipleIds( entityName ); } @@ -931,7 +932,7 @@ public class SessionDelegatorBaseImpl implements SessionImplementor { } @Override - public NaturalIdLoadAccess byNaturalId(String entityName) { + public NaturalIdLoadAccess byNaturalId(String entityName) { return delegate.byNaturalId( entityName ); } @@ -941,7 +942,7 @@ public class SessionDelegatorBaseImpl implements SessionImplementor { } @Override - public SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName) { + public SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName) { return delegate.bySimpleNaturalId( entityName ); } @@ -950,6 +951,16 @@ public class SessionDelegatorBaseImpl implements SessionImplementor { return delegate.bySimpleNaturalId( entityClass ); } + @Override + public NaturalIdMultiLoadAccess byMultipleNaturalId(Class entityClass) { + return delegate.byMultipleNaturalId( entityClass ); + } + + @Override + public NaturalIdMultiLoadAccess byMultipleNaturalId(String entityName) { + return delegate.byMultipleNaturalId( entityName ); + } + @Override public Filter enableFilter(String filterName) { return delegate.enableFilter( filterName ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractLockUpgradeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractLockUpgradeEventListener.java index fcffbcd4cd..9e0e66c78f 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractLockUpgradeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractLockUpgradeEventListener.java @@ -6,17 +6,13 @@ */ package org.hibernate.event.internal; -import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.ObjectDeletedException; -import org.hibernate.cache.spi.access.EntityDataAccess; -import org.hibernate.cache.spi.access.SoftLock; import org.hibernate.engine.spi.EntityEntry; -import org.hibernate.engine.spi.Status; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.internal.CoreLogging; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.pretty.MessageHelper; +import org.hibernate.loader.ast.internal.LoaderHelper; + import org.jboss.logging.Logger; /** @@ -37,60 +33,6 @@ public abstract class AbstractLockUpgradeEventListener extends AbstractReassocia * @param source The session which is the source of the event being processed. */ protected void upgradeLock(Object object, EntityEntry entry, LockOptions lockOptions, EventSource source) { - - LockMode requestedLockMode = lockOptions.getLockMode(); - if ( requestedLockMode.greaterThan( entry.getLockMode() ) ) { - // The user requested a "greater" (i.e. more restrictive) form of - // pessimistic lock - - if ( entry.getStatus() != Status.MANAGED ) { - throw new ObjectDeletedException( - "attempted to lock a deleted instance", - entry.getId(), - entry.getPersister().getEntityName() - ); - } - - final EntityPersister persister = entry.getPersister(); - - if ( log.isTraceEnabled() ) { - log.tracev( - "Locking {0} in mode: {1}", - MessageHelper.infoString( persister, entry.getId(), source.getFactory() ), - requestedLockMode - ); - } - - final boolean cachingEnabled = persister.canWriteToCache(); - SoftLock lock = null; - Object ck = null; - try { - if ( cachingEnabled ) { - EntityDataAccess cache = persister.getCacheAccessStrategy(); - ck = cache.generateCacheKey( entry.getId(), persister, source.getFactory(), source.getTenantIdentifier() ); - lock = cache.lockItem( source, ck, entry.getVersion() ); - } - - if ( persister.isVersioned() && requestedLockMode == LockMode.FORCE ) { - // todo : should we check the current isolation mode explicitly? - Object nextVersion = persister.forceVersionIncrement( - entry.getId(), entry.getVersion(), source - ); - entry.forceLocked( object, nextVersion ); - } - else { - persister.lock( entry.getId(), entry.getVersion(), object, lockOptions, source ); - } - entry.setLockMode(requestedLockMode); - } - finally { - // the database now holds a lock + the object is flushed from the cache, - // so release the soft lock - if ( cachingEnabled ) { - persister.getCacheAccessStrategy().unlockItem( source, ck, lock ); - } - } - - } + LoaderHelper.upgradeLock( object, entry, lockOptions, (SessionImplementor) source ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java new file mode 100644 index 0000000000..3b14a86287 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java @@ -0,0 +1,170 @@ +/* + * 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.internal; + +import java.util.List; +import java.util.function.Supplier; + +import org.hibernate.CacheMode; +import org.hibernate.LockOptions; +import org.hibernate.MultiIdentifierLoadAccess; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.RootGraph; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; + +/** + * @author Steve Ebersole + */ +class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, MultiIdLoadOptions { + private final SessionImpl session; + private final EntityPersister entityPersister; + + private LockOptions lockOptions; + private CacheMode cacheMode; + + private RootGraphImplementor rootGraph; + private GraphSemantic graphSemantic; + + private Integer batchSize; + private boolean sessionCheckingEnabled; + private boolean returnOfDeletedEntitiesEnabled; + private boolean orderedReturnEnabled = true; + + public MultiIdentifierLoadAccessImpl(SessionImpl session, EntityPersister entityPersister) { + this.session = session; + this.entityPersister = entityPersister; + } + + @Override + public LockOptions getLockOptions() { + return lockOptions; + } + + @Override + public final MultiIdentifierLoadAccess with(LockOptions lockOptions) { + this.lockOptions = lockOptions; + return this; + } + + @Override + public MultiIdentifierLoadAccess with(CacheMode cacheMode) { + this.cacheMode = cacheMode; + return this; + } + + @Override + public MultiIdentifierLoadAccess with(RootGraph graph, GraphSemantic semantic) { + this.rootGraph = (RootGraphImplementor) graph; + this.graphSemantic = semantic; + return this; + } + + @Override + public Integer getBatchSize() { + return batchSize; + } + + @Override + public MultiIdentifierLoadAccess withBatchSize(int batchSize) { + if ( batchSize < 1 ) { + this.batchSize = null; + } + else { + this.batchSize = batchSize; + } + return this; + } + + @Override + public boolean isSessionCheckingEnabled() { + return sessionCheckingEnabled; + } + + @Override + public boolean isSecondLevelCacheCheckingEnabled() { + return cacheMode == CacheMode.NORMAL || cacheMode == CacheMode.GET; + } + + @Override + public MultiIdentifierLoadAccess enableSessionCheck(boolean enabled) { + this.sessionCheckingEnabled = enabled; + return this; + } + + @Override + public boolean isReturnOfDeletedEntitiesEnabled() { + return returnOfDeletedEntitiesEnabled; + } + + @Override + public MultiIdentifierLoadAccess enableReturnOfDeletedEntities(boolean enabled) { + this.returnOfDeletedEntitiesEnabled = enabled; + return this; + } + + @Override + public boolean isOrderReturnEnabled() { + return orderedReturnEnabled; + } + + @Override + public MultiIdentifierLoadAccess enableOrderedReturn(boolean enabled) { + this.orderedReturnEnabled = enabled; + return this; + } + + @Override + @SuppressWarnings( "unchecked" ) + public List multiLoad(K... ids) { + return perform( () -> entityPersister.multiLoad( ids, session, this ) ); + } + + public List perform(Supplier> executor) { + CacheMode sessionCacheMode = session.getCacheMode(); + boolean cacheModeChanged = false; + if ( cacheMode != null ) { + // naive check for now... + // todo : account for "conceptually equal" + if ( cacheMode != sessionCacheMode ) { + session.setCacheMode( cacheMode ); + cacheModeChanged = true; + } + } + + try { + if ( graphSemantic != null ) { + if ( rootGraph == null ) { + throw new IllegalArgumentException( "Graph semantic specified, but no RootGraph was supplied" ); + } + session.getLoadQueryInfluencers().getEffectiveEntityGraph().applyGraph( rootGraph, graphSemantic ); + } + + try { + return executor.get(); + } + finally { + if ( graphSemantic != null ) { + session.getLoadQueryInfluencers().getEffectiveEntityGraph().clear(); + } + } + } + finally { + if ( cacheModeChanged ) { + // change it back + session.setCacheMode( sessionCacheMode ); + } + } + } + + @Override + @SuppressWarnings( "unchecked" ) + public List multiLoad(List ids) { + return perform( () -> entityPersister.multiLoad( ids.toArray( new Object[ 0 ] ), session, this ) ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java new file mode 100644 index 0000000000..e61281fabc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -0,0 +1,158 @@ +/* + * 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.internal; + +import java.util.List; + +import org.hibernate.CacheMode; +import org.hibernate.LockOptions; +import org.hibernate.NaturalIdMultiLoadAccess; +import org.hibernate.engine.spi.EffectiveEntityGraph; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.RootGraph; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.persister.entity.EntityPersister; + +/** + * @author Steve Ebersole + */ +public class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess, MultiNaturalIdLoadOptions { + private final EntityPersister entityDescriptor; + private final SessionImpl session; + + private LockOptions lockOptions; + private CacheMode cacheMode; + + private RootGraphImplementor rootGraph; + private GraphSemantic graphSemantic; + + private Integer batchSize; + private boolean returnOfDeletedEntitiesEnabled; + private boolean orderedReturnEnabled = true; + + public NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SessionImpl session) { + this.entityDescriptor = entityDescriptor; + this.session = session; + } + + @Override + public NaturalIdMultiLoadAccess with(LockOptions lockOptions) { + this.lockOptions = lockOptions; + return this; + } + + @Override + public NaturalIdMultiLoadAccess with(CacheMode cacheMode) { + this.cacheMode = cacheMode; + return this; + } + + @Override + public NaturalIdMultiLoadAccess with(RootGraph graph, GraphSemantic semantic) { + this.rootGraph = (RootGraphImplementor) graph; + this.graphSemantic = semantic; + return this; + } + + @Override + public NaturalIdMultiLoadAccess withBatchSize(int batchSize) { + this.batchSize = batchSize; + return this; + } + + @Override + public NaturalIdMultiLoadAccess enableReturnOfDeletedEntities(boolean enabled) { + returnOfDeletedEntitiesEnabled = enabled; + return this; + } + + @Override + public NaturalIdMultiLoadAccess enableOrderedReturn(boolean enabled) { + orderedReturnEnabled = enabled; + return this; + } + + @Override + @SuppressWarnings( "unchecked" ) + public List multiLoad(Object... ids) { + final CacheMode sessionCacheMode = session.getCacheMode(); + boolean cacheModeChanged = false; + + if ( cacheMode != null ) { + // naive check for now... + // todo : account for "conceptually equal" + if ( cacheMode != sessionCacheMode ) { + session.setCacheMode( cacheMode ); + cacheModeChanged = true; + } + } + + final LoadQueryInfluencers loadQueryInfluencers = session.getLoadQueryInfluencers(); + + try { + final EffectiveEntityGraph effectiveEntityGraph = loadQueryInfluencers.getEffectiveEntityGraph(); + final GraphSemantic initialGraphSemantic = effectiveEntityGraph.getSemantic(); + final RootGraphImplementor initialGraph = effectiveEntityGraph.getGraph(); + final boolean hadInitialGraph = initialGraphSemantic != null; + + if ( graphSemantic != null ) { + if ( rootGraph == null ) { + throw new IllegalArgumentException( "Graph semantic specified, but no RootGraph was supplied" ); + } + effectiveEntityGraph.applyGraph( rootGraph, graphSemantic ); + } + + try { + return entityDescriptor.getNaturalIdMapping().getMultiNaturalIdLoader().multiLoad( ids, this, session ); + } + finally { + if ( graphSemantic != null ) { + if ( hadInitialGraph ) { + effectiveEntityGraph.applyGraph( initialGraph, initialGraphSemantic ); + } + else { + effectiveEntityGraph.clear(); + } + } + } + } + finally { + if ( cacheModeChanged ) { + // change it back + session.setCacheMode( sessionCacheMode ); + } + } + + } + + @Override + public List multiLoad(List ids) { + return multiLoad( ids.toArray( new Object[ 0 ] ) ); + } + + @Override + public boolean isReturnOfDeletedEntitiesEnabled() { + return returnOfDeletedEntitiesEnabled; + } + + @Override + public boolean isOrderReturnEnabled() { + return orderedReturnEnabled; + } + + @Override + public LockOptions getLockOptions() { + return lockOptions; + } + + @Override + public Integer getBatchSize() { + return batchSize; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index d920ccac2d..465f3e74a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -56,6 +56,7 @@ import org.hibernate.SessionEventListener; import org.hibernate.SessionException; import org.hibernate.SharedSessionBuilder; import org.hibernate.SimpleNaturalIdLoadAccess; +import org.hibernate.NaturalIdMultiLoadAccess; import org.hibernate.Transaction; import org.hibernate.TransientObjectException; import org.hibernate.TypeMismatchException; @@ -114,6 +115,7 @@ import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.RootGraph; import org.hibernate.graph.internal.RootGraphImpl; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.jpa.AvailableSettings; import org.hibernate.jpa.QueryHints; import org.hibernate.jpa.internal.util.CacheModeHelper; @@ -121,9 +123,9 @@ import org.hibernate.jpa.internal.util.ConfigurationHelper; import org.hibernate.jpa.internal.util.FlushModeTypeHelper; import org.hibernate.jpa.internal.util.LockModeTypeHelper; import org.hibernate.jpa.internal.util.LockOptionsHelper; +import org.hibernate.loader.ast.spi.NaturalIdLoader; import org.hibernate.metamodel.spi.MetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.MultiLoadOptions; import org.hibernate.pretty.MessageHelper; import org.hibernate.procedure.ProcedureCall; import org.hibernate.procedure.spi.NamedCallableQueryMemento; @@ -1105,8 +1107,8 @@ public class SessionImpl } @Override - public IdentifierLoadAccessImpl byId(String entityName) { - return new IdentifierLoadAccessImpl( entityName ); + public IdentifierLoadAccessImpl byId(String entityName) { + return new IdentifierLoadAccessImpl<>( entityName ); } @Override @@ -1116,17 +1118,17 @@ public class SessionImpl @Override public MultiIdentifierLoadAccess byMultipleIds(Class entityClass) { - return new MultiIdentifierLoadAccessImpl<>( locateEntityPersister( entityClass ) ); + return new MultiIdentifierLoadAccessImpl<>( this, requireEntityPersister( entityClass ) ); } @Override - public MultiIdentifierLoadAccess byMultipleIds(String entityName) { - return new MultiIdentifierLoadAccessImpl( locateEntityPersister( entityName ) ); + public MultiIdentifierLoadAccess byMultipleIds(String entityName) { + return new MultiIdentifierLoadAccessImpl<>( this, requireEntityPersister( entityName ) ); } @Override - public NaturalIdLoadAccess byNaturalId(String entityName) { - return new NaturalIdLoadAccessImpl( entityName ); + public NaturalIdLoadAccess byNaturalId(String entityName) { + return new NaturalIdLoadAccessImpl<>( entityName ); } @Override @@ -1135,8 +1137,8 @@ public class SessionImpl } @Override - public SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName) { - return new SimpleNaturalIdLoadAccessImpl( entityName ); + public SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName) { + return new SimpleNaturalIdLoadAccessImpl<>( entityName ); } @Override @@ -1144,6 +1146,16 @@ public class SessionImpl return new SimpleNaturalIdLoadAccessImpl<>( entityClass ); } + @Override + public NaturalIdMultiLoadAccess byMultipleNaturalId(Class entityClass) { + return new NaturalIdMultiLoadAccessStandard<>( requireEntityPersister( entityClass ), this ); + } + + @Override + public NaturalIdMultiLoadAccess byMultipleNaturalId(String entityName) { + return new NaturalIdMultiLoadAccessStandard<>( requireEntityPersister( entityName ), this ); + } + private void fireLoad(LoadEvent event, LoadType loadType) { checkOpenOrWaitingForAutoClose(); fireLoadNoChecks( event, loadType ); @@ -2109,11 +2121,11 @@ public class SessionImpl } private IdentifierLoadAccessImpl(String entityName) { - this( locateEntityPersister( entityName ) ); + this( requireEntityPersister( entityName ) ); } private IdentifierLoadAccessImpl(Class entityClass) { - this( locateEntityPersister( entityClass ) ); + this( requireEntityPersister( entityClass ) ); } @Override @@ -2237,157 +2249,11 @@ public class SessionImpl } } - private class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, MultiLoadOptions { - private final EntityPersister entityPersister; - - private LockOptions lockOptions; - private CacheMode cacheMode; - - private RootGraphImplementor rootGraph; - private GraphSemantic graphSemantic; - - private Integer batchSize; - private boolean sessionCheckingEnabled; - private boolean returnOfDeletedEntitiesEnabled; - private boolean orderedReturnEnabled = true; - - public MultiIdentifierLoadAccessImpl(EntityPersister entityPersister) { - this.entityPersister = entityPersister; - } - - @Override - public LockOptions getLockOptions() { - return lockOptions; - } - - @Override - public final MultiIdentifierLoadAccess with(LockOptions lockOptions) { - this.lockOptions = lockOptions; - return this; - } - - @Override - public MultiIdentifierLoadAccess with(CacheMode cacheMode) { - this.cacheMode = cacheMode; - return this; - } - - @Override - public MultiIdentifierLoadAccess with(RootGraph graph, GraphSemantic semantic) { - this.rootGraph = (RootGraphImplementor) graph; - this.graphSemantic = semantic; - return this; - } - - @Override - public Integer getBatchSize() { - return batchSize; - } - - @Override - public MultiIdentifierLoadAccess withBatchSize(int batchSize) { - if ( batchSize < 1 ) { - this.batchSize = null; - } - else { - this.batchSize = batchSize; - } - return this; - } - - @Override - public boolean isSessionCheckingEnabled() { - return sessionCheckingEnabled; - } - - @Override - public boolean isSecondLevelCacheCheckingEnabled() { - return cacheMode == CacheMode.NORMAL || cacheMode == CacheMode.GET; - } - - @Override - public MultiIdentifierLoadAccess enableSessionCheck(boolean enabled) { - this.sessionCheckingEnabled = enabled; - return this; - } - - @Override - public boolean isReturnOfDeletedEntitiesEnabled() { - return returnOfDeletedEntitiesEnabled; - } - - @Override - public MultiIdentifierLoadAccess enableReturnOfDeletedEntities(boolean enabled) { - this.returnOfDeletedEntitiesEnabled = enabled; - return this; - } - - @Override - public boolean isOrderReturnEnabled() { - return orderedReturnEnabled; - } - - @Override - public MultiIdentifierLoadAccess enableOrderedReturn(boolean enabled) { - this.orderedReturnEnabled = enabled; - return this; - } - - @Override - @SuppressWarnings("unchecked") - public List multiLoad(K... ids) { - return perform( () -> entityPersister.multiLoad( ids, SessionImpl.this, this ) ); - } - - public List perform(Supplier> executor) { - CacheMode sessionCacheMode = getCacheMode(); - boolean cacheModeChanged = false; - if ( cacheMode != null ) { - // naive check for now... - // todo : account for "conceptually equal" - if ( cacheMode != sessionCacheMode ) { - setCacheMode( cacheMode ); - cacheModeChanged = true; - } - } - - try { - if ( graphSemantic != null ) { - if ( rootGraph == null ) { - throw new IllegalArgumentException( "Graph semantic specified, but no RootGraph was supplied" ); - } - loadQueryInfluencers.getEffectiveEntityGraph().applyGraph( rootGraph, graphSemantic ); - } - - try { - return executor.get(); - } - finally { - if ( graphSemantic != null ) { - loadQueryInfluencers.getEffectiveEntityGraph().clear(); - } - } - } - finally { - if ( cacheModeChanged ) { - // change it back - setCacheMode( sessionCacheMode ); - } - } - } - - @Override - @SuppressWarnings("unchecked") - public List multiLoad(List ids) { - return perform( () -> entityPersister.multiLoad( ids.toArray( new Object[0] ), SessionImpl.this, this ) ); - } - } - - private EntityPersister locateEntityPersister(Class entityClass) { + private EntityPersister requireEntityPersister(Class entityClass) { return getFactory().getMetamodel().locateEntityPersister( entityClass ); } - private EntityPersister locateEntityPersister(String entityName) { + private EntityPersister requireEntityPersister(String entityName) { return getFactory().getMetamodel().locateEntityPersister( entityName ); } @@ -2406,6 +2272,14 @@ public class SessionImpl } } + public LockOptions getLockOptions() { + return lockOptions; + } + + public boolean isSynchronizationEnabled() { + return synchronizationEnabled; + } + public BaseNaturalIdLoadAccessImpl with(LockOptions lockOptions) { this.lockOptions = lockOptions; return this; @@ -2418,16 +2292,14 @@ public class SessionImpl protected final Object resolveNaturalId(Map naturalIdParameters) { performAnyNeededCrossReferenceSynchronizations(); - final ResolveNaturalIdEvent event = - new ResolveNaturalIdEvent( naturalIdParameters, entityPersister, SessionImpl.this ); - fireResolveNaturalId( event ); + final Object resolvedId = entityPersister() + .getNaturalIdMapping() + .getNaturalIdLoader() + .resolveNaturalIdToId( naturalIdParameters, SessionImpl.this ); - if ( event.getEntityId() == PersistenceContext.NaturalIdHelper.INVALID_NATURAL_ID_REFERENCE ) { - return null; - } - else { - return event.getEntityId(); - } + return resolvedId == PersistenceContext.NaturalIdHelper.INVALID_NATURAL_ID_REFERENCE + ? null + : resolvedId; } protected void performAnyNeededCrossReferenceSynchronizations() { @@ -2500,11 +2372,11 @@ public class SessionImpl } private NaturalIdLoadAccessImpl(String entityName) { - this( locateEntityPersister( entityName ) ); + this( requireEntityPersister( entityName ) ); } private NaturalIdLoadAccessImpl(Class entityClass) { - this( locateEntityPersister( entityClass ) ); + this( requireEntityPersister( entityClass ) ); } @Override @@ -2518,6 +2390,12 @@ public class SessionImpl return this; } + @Override + public NaturalIdLoadAccess using(Object... mappings) { + CollectionHelper.collectMapEntries( naturalIdParameters::put, mappings ); + return this; + } + @Override public NaturalIdLoadAccessImpl setSynchronizationEnabled(boolean synchronizationEnabled) { super.synchronizationEnabled( synchronizationEnabled ); @@ -2556,8 +2434,9 @@ public class SessionImpl } } - private class SimpleNaturalIdLoadAccessImpl extends BaseNaturalIdLoadAccessImpl - implements SimpleNaturalIdLoadAccess { + private class SimpleNaturalIdLoadAccessImpl + extends BaseNaturalIdLoadAccessImpl + implements SimpleNaturalIdLoadAccess, NaturalIdLoader.LoadOptions { private final String naturalIdAttributeName; private SimpleNaturalIdLoadAccessImpl(EntityPersister entityPersister) { @@ -2577,11 +2456,21 @@ public class SessionImpl } private SimpleNaturalIdLoadAccessImpl(String entityName) { - this( locateEntityPersister( entityName ) ); + this( requireEntityPersister( entityName ) ); + } + + @Override + public LockOptions getLockOptions() { + return super.getLockOptions(); + } + + @Override + public boolean isSynchronizationEnabled() { + return super.isSynchronizationEnabled(); } private SimpleNaturalIdLoadAccessImpl(Class entityClass) { - this( locateEntityPersister( entityClass ) ); + this( requireEntityPersister( entityClass ) ); } @Override @@ -2602,7 +2491,7 @@ public class SessionImpl @Override @SuppressWarnings("unchecked") public T getReference(Object naturalIdValue) { - final Object entityId = resolveNaturalId( getNaturalIdParameters( naturalIdValue ) ); + final Object entityId = entityPersister().getNaturalIdLoader().resolveNaturalIdToId( naturalIdValue, SessionImpl.this ); if ( entityId == null ) { return null; } @@ -2610,19 +2499,9 @@ public class SessionImpl } @Override - @SuppressWarnings("unchecked") public T load(Object naturalIdValue) { - final Object entityId = resolveNaturalId( getNaturalIdParameters( naturalIdValue ) ); - if ( entityId == null ) { - return null; - } - try { - return (T) this.getIdentifierLoadAccess().load( entityId ); - } - catch (EntityNotFoundException | ObjectNotFoundException e) { - // OK - } - return null; + //noinspection unchecked + return (T) entityPersister().getNaturalIdLoader().load( naturalIdValue, this, SessionImpl.this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/MutableInteger.java b/hibernate-core/src/main/java/org/hibernate/internal/util/MutableInteger.java index 4c19a06d0d..738e3fa03d 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/MutableInteger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/MutableInteger.java @@ -48,4 +48,12 @@ public class MutableInteger { public void increase() { ++value; } + + public void plus(int i) { + value += i; + } + + public void minus(int i) { + value -= i; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/CollectionHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/CollectionHelper.java index 19aa1b9389..f988bc569e 100755 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/collections/CollectionHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/collections/CollectionHelper.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.Function; /** @@ -361,4 +362,27 @@ public final class CollectionHelper { } } + @SuppressWarnings( "unchecked" ) + public static void collectMapEntries(BiConsumer mapEntryConsumer, Object[] mappings) { + // even numbered + assert mappings.length %2 == 0; + + for ( int i = 0; i < mappings.length; i += 2 ) { + mapEntryConsumer.accept( (K) mappings[i], (V) mappings[i+1] ); + } + } + + @SuppressWarnings( "unchecked" ) + public static Map asMap(Object[] elements) { + assert elements != null; + assert elements.length % 2 == 0; + + final HashMap map = new HashMap<>(); + collectMapEntries( map::put, elements ); + for ( int i = 0; i < elements.length; i += 2 ) { + map.put( (K) elements[ i ], (S) elements[ i+1 ] ); + } + + return map; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/NaturalIdPostLoadListener.java b/hibernate-core/src/main/java/org/hibernate/loader/NaturalIdPostLoadListener.java new file mode 100644 index 0000000000..34638b2745 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/NaturalIdPostLoadListener.java @@ -0,0 +1,26 @@ +/* + * 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.loader; + +import org.hibernate.SharedSessionContract; +import org.hibernate.metamodel.mapping.EntityMappingType; + +/** + * Listener for post load-by-natural-id events + */ +@FunctionalInterface +public interface NaturalIdPostLoadListener { + /** + * Singleton access for no listener + */ + NaturalIdPostLoadListener NO_OP = (loadingEntity, entity, session) -> {}; + + /** + * Callback for a load-by-natural-id pre event + */ + void completedLoadByNaturalId(EntityMappingType entityMappingType, Object entity, SharedSessionContract session); +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/NaturalIdPreLoadListener.java b/hibernate-core/src/main/java/org/hibernate/loader/NaturalIdPreLoadListener.java new file mode 100644 index 0000000000..2a4efd2605 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/NaturalIdPreLoadListener.java @@ -0,0 +1,26 @@ +/* + * 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.loader; + +import org.hibernate.SharedSessionContract; +import org.hibernate.metamodel.mapping.EntityMappingType; + +/** + * Listener for pre load-by-natural-id events + */ +@FunctionalInterface +public interface NaturalIdPreLoadListener { + /** + * Singleton access for no listener + */ + NaturalIdPreLoadListener NO_OP = (loadingEntity, naturalId, session) -> {}; + + /** + * Callback for a load-by-natural-id pre event + */ + void startingLoadByNaturalId(EntityMappingType loadingEntity, Object naturalId, SharedSessionContract session); +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/LoaderLogging.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/LoaderLogging.java new file mode 100644 index 0000000000..f751e5d625 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/LoaderLogging.java @@ -0,0 +1,21 @@ +/* + * 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.loader.ast; + +import org.jboss.logging.Logger; + +/** + * Logging for loaders + */ +public interface LoaderLogging { + String LOGGER_NAME = "org.hibernate.orm.loader"; + + Logger LOADER_LOGGER = Logger.getLogger( LOGGER_NAME ); + + boolean DEBUG_ENABLED = LOADER_LOGGER.isDebugEnabled(); + boolean TRACE_ENABLED = LOADER_LOGGER.isTraceEnabled(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/NaturalIdLoaderStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java similarity index 67% rename from hibernate-core/src/main/java/org/hibernate/loader/ast/internal/NaturalIdLoaderStandardImpl.java rename to hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java index d2364aa6ae..d32ed9c384 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/NaturalIdLoaderStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java @@ -17,11 +17,13 @@ import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.StringHelper; +import org.hibernate.loader.NaturalIdPostLoadListener; +import org.hibernate.loader.NaturalIdPreLoadListener; +import org.hibernate.loader.ast.spi.Loadable; import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.NaturalIdMapping; -import org.hibernate.metamodel.mapping.SingularAttributeMapping; -import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.sql.ast.Clause; @@ -36,71 +38,72 @@ import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcSelect; /** - * @author Steve Ebersole + * Base support for NaturalIdLoader implementations */ -public class NaturalIdLoaderStandardImpl implements NaturalIdLoader { - private final EntityPersister entityDescriptor; - private final NaturalIdMapping naturalIdMapping; +public abstract class AbstractNaturalIdLoader implements NaturalIdLoader { - public NaturalIdLoaderStandardImpl(EntityPersister entityDescriptor) { + // todo (6.0) : account for nullable attributes that are part of the natural-id (is-null-or-equals) + // todo (6.0) : cache the SQL AST and JdbcParameter list + + private final NaturalIdMapping naturalIdMapping; + private final EntityMappingType entityDescriptor; + + private final NaturalIdPreLoadListener preLoadListener; + private final NaturalIdPostLoadListener postLoadListener; + + public AbstractNaturalIdLoader( + NaturalIdMapping naturalIdMapping, + NaturalIdPreLoadListener preLoadListener, + NaturalIdPostLoadListener postLoadListener, + EntityMappingType entityDescriptor, + MappingModelCreationProcess creationProcess) { + this.naturalIdMapping = naturalIdMapping; + this.preLoadListener = preLoadListener; + this.postLoadListener = postLoadListener; this.entityDescriptor = entityDescriptor; - this.naturalIdMapping = entityDescriptor.getNaturalIdMapping(); - - if ( ! entityDescriptor.hasNaturalIdentifier() ) { - throw new HibernateException( "Entity does not define natural-id : " + entityDescriptor.getEntityName() ); - } - - // todo (6.0) : account for nullable attributes that are part of the natural-id (is-null-or-equals) - // todo (6.0) : cache the SQL AST and JdbcParameter list } - @Override - public EntityPersister getLoadable() { + protected EntityMappingType entityDescriptor() { return entityDescriptor; } + protected NaturalIdMapping naturalIdMapping() { + return naturalIdMapping; + } + @Override - public T load(Object naturalIdToLoad, LoadOptions options, SharedSessionContractImplementor session) { + public Loadable getLoadable() { + return entityDescriptor(); + } + + @Override + public T load(Object naturalIdValue, LoadOptions options, SharedSessionContractImplementor session) { + final Object bindValue = resolveNaturalIdBindValue( naturalIdValue, session ); + preLoadListener.startingLoadByNaturalId( entityDescriptor, bindValue, session ); + final SessionFactoryImplementor sessionFactory = session.getFactory(); - - final List jdbcParameters = new ArrayList<>(); - final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( - entityDescriptor, - Collections.emptyList(), - naturalIdMapping, - null, - 1, - session.getLoadQueryInfluencers(), - LockOptions.READ, - jdbcParameters::add, - sessionFactory - ); - final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); + final List jdbcParameters = new ArrayList<>(); + final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( + entityDescriptor(), + Collections.emptyList(), + naturalIdMapping(), + null, + 1, + session.getLoadQueryInfluencers(), + options.getLockOptions(), + jdbcParameters::add, + sessionFactory + ); + final JdbcSelect jdbcSelect = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ).translate( sqlSelect ); final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); - final Iterator jdbcParamItr = jdbcParameters.iterator(); - for ( int i = 0; i < naturalIdMapping.getNaturalIdAttributes().size(); i++ ) { - final SingularAttributeMapping attrMapping = naturalIdMapping.getNaturalIdAttributes().get( i ); - attrMapping.visitJdbcValues( - naturalIdToLoad, - Clause.WHERE, - (jdbcValue, jdbcMapping) -> { - assert jdbcParamItr.hasNext(); - final JdbcParameter jdbcParam = jdbcParamItr.next(); - jdbcParamBindings.addBinding( - jdbcParam, - new JdbcParameterBindingImpl( jdbcMapping, jdbcValue ) - ); - }, - session - ); - } + applyNaturalIdAsJdbcParameters( bindValue, jdbcParameters, jdbcParamBindings, session ); //noinspection unchecked final List results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( @@ -141,18 +144,102 @@ public class NaturalIdLoaderStandardImpl implements NaturalIdLoader { ); } + final T result = results.get( 0 ); + postLoadListener.completedLoadByNaturalId( entityDescriptor, result, session ); + + return result; + } + + protected abstract Object resolveNaturalIdBindValue(Object naturalIdToLoad, SharedSessionContractImplementor session); + + protected abstract void applyNaturalIdAsJdbcParameters( + Object naturalIdToLoad, + List jdbcParameters, + JdbcParameterBindings jdbcParamBindings, SharedSessionContractImplementor session); + + @Override + public Object resolveNaturalIdToId(Object naturalIdValue, SharedSessionContractImplementor session) { + final Object bindValue = resolveNaturalIdBindValue( naturalIdValue, session ); + + final SessionFactoryImplementor sessionFactory = session.getFactory(); + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); + + final List jdbcParameters = new ArrayList<>(); + final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( + entityDescriptor(), + Collections.singletonList( entityDescriptor().getIdentifierMapping() ), + naturalIdMapping(), + null, + 1, + session.getLoadQueryInfluencers(), + LockOptions.READ, + jdbcParameters::add, + sessionFactory + ); + + final JdbcSelect jdbcSelect = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ).translate( sqlSelect ); + + final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); + applyNaturalIdAsJdbcParameters( + bindValue, + jdbcParameters, + jdbcParamBindings, + session + ); + + final List results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( + jdbcSelect, + jdbcParamBindings, + new ExecutionContext() { + @Override + public SharedSessionContractImplementor getSession() { + return session; + } + + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + + @Override + public QueryParameterBindings getQueryParameterBindings() { + return QueryParameterBindings.NO_PARAM_BINDINGS; + } + + @Override + public Callback getCallback() { + return afterLoadAction -> { + }; + } + }, + row -> row[0], + true + ); + + if ( results.size() > 1 ) { + throw new HibernateException( + String.format( + "Resolving natural-id to id returned more that one row : %s [%s]", + entityDescriptor().getEntityName(), + bindValue + ) + ); + } + return results.get( 0 ); } @Override - public Object[] resolveIdToNaturalId(Object id, SharedSessionContractImplementor session) { + public Object resolveIdToNaturalId(Object id, SharedSessionContractImplementor session) { final SessionFactoryImplementor sessionFactory = session.getFactory(); final List jdbcParameters = new ArrayList<>(); final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( - entityDescriptor, - naturalIdMapping.getNaturalIdAttributes(), - entityDescriptor.getIdentifierMapping(), + entityDescriptor(), + naturalIdMapping().getNaturalIdAttributes(), + entityDescriptor().getIdentifierMapping(), null, 1, session.getLoadQueryInfluencers(), @@ -170,7 +257,7 @@ public class NaturalIdLoaderStandardImpl implements NaturalIdLoader { final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); final Iterator jdbcParamItr = jdbcParameters.iterator(); - entityDescriptor.getIdentifierMapping().visitJdbcValues( + entityDescriptor().getIdentifierMapping().visitJdbcValues( id, Clause.WHERE, (value, type) -> { @@ -218,99 +305,19 @@ public class NaturalIdLoaderStandardImpl implements NaturalIdLoader { throw new HibernateException( String.format( "Resolving id to natural-id returned more that one row : %s #%s", - entityDescriptor.getEntityName(), + entityDescriptor().getEntityName(), id ) ); } - return results.get( 0 ); - } - - @Override - public Object resolveNaturalIdToId( - Object[] naturalIdValues, - SharedSessionContractImplementor session) { - final SessionFactoryImplementor sessionFactory = session.getFactory(); - - final List jdbcParameters = new ArrayList<>(); - final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( - entityDescriptor, - Collections.singletonList( entityDescriptor.getIdentifierMapping() ), - naturalIdMapping, - null, - 1, - session.getLoadQueryInfluencers(), - LockOptions.READ, - jdbcParameters::add, - sessionFactory - ); - - final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); - final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); - final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); - - final JdbcSelect jdbcSelect = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ).translate( sqlSelect ); - - final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); - final Iterator jdbcParamItr = jdbcParameters.iterator(); - - for ( int i = 0; i < naturalIdMapping.getNaturalIdAttributes().size(); i++ ) { - final SingularAttributeMapping attrMapping = naturalIdMapping.getNaturalIdAttributes().get( i ); - attrMapping.visitJdbcValues( - naturalIdValues[i], - Clause.WHERE, - (jdbcValue, jdbcMapping) -> { - assert jdbcParamItr.hasNext(); - jdbcParamBindings.addBinding( - jdbcParamItr.next(), - new JdbcParameterBindingImpl( jdbcMapping, jdbcValue ) - ); - }, - session - ); - } - assert !jdbcParamItr.hasNext(); - - final List results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( - jdbcSelect, - jdbcParamBindings, - new ExecutionContext() { - @Override - public SharedSessionContractImplementor getSession() { - return session; - } - - @Override - public QueryOptions getQueryOptions() { - return QueryOptions.NONE; - } - - @Override - public QueryParameterBindings getQueryParameterBindings() { - return QueryParameterBindings.NO_PARAM_BINDINGS; - } - - @Override - public Callback getCallback() { - return afterLoadAction -> { - }; - } - }, - row -> row, - true - ); - - if ( results.size() > 1 ) { - throw new HibernateException( - String.format( - "Resolving natural-id to id returned more that one row : %s [%s]", - entityDescriptor.getEntityName(), - StringHelper.join( ", ", naturalIdValues ) - ) - ); + final Object[] objects = results.get( 0 ); + if ( isSimple() ) { + return objects[0]; } - return results.get( 0 ); + return objects; } + + protected abstract boolean isSimple(); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java new file mode 100644 index 0000000000..90355e173a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java @@ -0,0 +1,98 @@ +/* + * 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.loader.ast.internal; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.NaturalIdPostLoadListener; +import org.hibernate.loader.NaturalIdPreLoadListener; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.metamodel.mapping.internal.CompoundNaturalIdMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +/** + * NaturalIdLoader implementation for compound natural-ids + */ +public class CompoundNaturalIdLoader extends AbstractNaturalIdLoader { + + public CompoundNaturalIdLoader( + CompoundNaturalIdMapping naturalIdMapping, + NaturalIdPreLoadListener preLoadListener, + NaturalIdPostLoadListener postLoadListener, + EntityMappingType entityDescriptor, + MappingModelCreationProcess creationProcess) { + super( naturalIdMapping, preLoadListener, postLoadListener, entityDescriptor, creationProcess ); + } + + @Override + protected Object resolveNaturalIdBindValue(Object naturalIdValue, SharedSessionContractImplementor session) { + // the "real" form as an array, although we also accept here a Map and reduce it to + // the appropriately ordered array + if ( naturalIdValue instanceof Object[] ) { + return naturalIdValue; + } + + final List attributes = naturalIdMapping().getNaturalIdAttributes(); + final Object[] naturalId = new Object[ attributes.size() ]; + + if ( naturalIdValue instanceof Map ) { + final Map valueMap = (Map) naturalIdValue; + for ( int i = 0; i < attributes.size(); i++ ) { + final SingularAttributeMapping attributeMapping = attributes.get( i ); + naturalId[ i ] = valueMap.get( attributeMapping.getAttributeName() ); + } + return naturalId; + } + + throw new IllegalArgumentException( "Unexpected natural-id reference [" + naturalIdValue + "; expecting array or Map" ); + } + + @Override + protected void applyNaturalIdAsJdbcParameters( + Object naturalIdToLoad, + List jdbcParameters, + JdbcParameterBindings jdbcParamBindings, + SharedSessionContractImplementor session) { + assert naturalIdToLoad instanceof Object[]; + final Object[] naturalIdValueArray = (Object[]) naturalIdToLoad; + + final Iterator jdbcParamItr = jdbcParameters.iterator(); + + for ( int i = 0; i < naturalIdMapping().getNaturalIdAttributes().size(); i++ ) { + final SingularAttributeMapping attrMapping = naturalIdMapping().getNaturalIdAttributes().get( i ); + attrMapping.visitJdbcValues( + naturalIdValueArray[i], + Clause.WHERE, + (jdbcValue, jdbcMapping) -> { + assert jdbcParamItr.hasNext(); + final JdbcParameter jdbcParam = jdbcParamItr.next(); + jdbcParamBindings.addBinding( + jdbcParam, + new JdbcParameterBindingImpl( jdbcMapping, jdbcValue ) + ); + }, + session + ); + } + + // make sure we've exhausted all JDBC parameters + assert ! jdbcParamItr.hasNext(); + } + + @Override + protected boolean isSimple() { + return false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java new file mode 100644 index 0000000000..047bc59104 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderHelper.java @@ -0,0 +1,82 @@ +/* + * 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.loader.ast.internal; + +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.ObjectDeletedException; +import org.hibernate.cache.spi.access.EntityDataAccess; +import org.hibernate.cache.spi.access.SoftLock; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.loader.ast.LoaderLogging; +import org.hibernate.persister.entity.EntityPersister; + +/** + * @author Steve Ebersole + */ +public class LoaderHelper { + + public static void upgradeLock(Object object, EntityEntry entry, LockOptions lockOptions, SharedSessionContractImplementor session) { + LockMode requestedLockMode = lockOptions.getLockMode(); + if ( requestedLockMode.greaterThan( entry.getLockMode() ) ) { + // The user requested a "greater" (i.e. more restrictive) form of + // pessimistic lock + + if ( entry.getStatus() != Status.MANAGED ) { + throw new ObjectDeletedException( + "attempted to lock a deleted instance", + entry.getId(), + entry.getPersister().getEntityName() + ); + } + + final EntityPersister persister = entry.getPersister(); + + if ( LoaderLogging.TRACE_ENABLED ) { + LoaderLogging.LOADER_LOGGER.tracef( + "Locking `%s( %s )` in `%s` lock-mode", + persister.getEntityName(), + entry.getId(), + requestedLockMode + ); + } + + final boolean cachingEnabled = persister.canWriteToCache(); + SoftLock lock = null; + Object ck = null; + try { + if ( cachingEnabled ) { + EntityDataAccess cache = persister.getCacheAccessStrategy(); + ck = cache.generateCacheKey( entry.getId(), persister, session.getFactory(), session.getTenantIdentifier() ); + lock = cache.lockItem( session, ck, entry.getVersion() ); + } + + if ( persister.isVersioned() && requestedLockMode == LockMode.FORCE ) { + // todo : should we check the current isolation mode explicitly? + Object nextVersion = persister.forceVersionIncrement( + entry.getId(), entry.getVersion(), session + ); + entry.forceLocked( object, nextVersion ); + } + else { + persister.lock( entry.getId(), entry.getVersion(), object, lockOptions, session ); + } + entry.setLockMode(requestedLockMode); + } + finally { + // the database now holds a lock + the object is flushed from the cache, + // so release the soft lock + if ( cachingEnabled ) { + persister.getCacheAccessStrategy().unlockItem( session, ck, lock ); + } + } + + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java index adf84c4126..b8412bd82b 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -39,6 +40,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; import org.hibernate.metamodel.mapping.internal.SimpleForeignKeyDescriptor; @@ -109,7 +111,7 @@ public class LoaderSelectBuilder { Loadable loadable, List partsToSelect, ModelPart restrictedPart, - DomainResult cachedDomainResult, + DomainResult cachedDomainResult, int numberOfKeysToLoad, LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, @@ -135,7 +137,7 @@ public class LoaderSelectBuilder { Loadable loadable, List partsToSelect, List restrictedParts, - DomainResult cachedDomainResult, + DomainResult cachedDomainResult, int numberOfKeysToLoad, LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, @@ -216,6 +218,8 @@ public class LoaderSelectBuilder { int numberOfKeysToLoad, LoadQueryInfluencers loadQueryInfluencers, LockOptions lockOptions, + EntityGraphTraversalState entityGraphTraversalState, + boolean forceIdentifierSelection, Consumer jdbcParameterConsumer) { this.creationContext = creationContext; this.loadable = loadable; @@ -224,32 +228,70 @@ public class LoaderSelectBuilder { this.cachedDomainResult = cachedDomainResult; this.numberOfKeysToLoad = numberOfKeysToLoad; this.loadQueryInfluencers = loadQueryInfluencers; + this.lockOptions = lockOptions; + this.entityGraphTraversalState = entityGraphTraversalState; + this.forceIdentifierSelection = forceIdentifierSelection; + this.jdbcParameterConsumer = jdbcParameterConsumer; + } + + private LoaderSelectBuilder( + SqlAstCreationContext creationContext, + Loadable loadable, + List partsToSelect, + List restrictedParts, + DomainResult cachedDomainResult, + int numberOfKeysToLoad, + LoadQueryInfluencers loadQueryInfluencers, + LockOptions lockOptions, + Consumer jdbcParameterConsumer) { + this.creationContext = creationContext; + this.loadable = loadable; + this.partsToSelect = partsToSelect; + this.restrictedParts = restrictedParts; + this.cachedDomainResult = cachedDomainResult; + this.numberOfKeysToLoad = numberOfKeysToLoad; + this.loadQueryInfluencers = loadQueryInfluencers; + + this.forceIdentifierSelection = determineWhetherToForceIdSelection( numberOfKeysToLoad, restrictedParts ); + this.entityGraphTraversalState = determineGraphTraversalState( loadQueryInfluencers ); + + this.lockOptions = lockOptions != null ? lockOptions : LockOptions.NONE; + this.jdbcParameterConsumer = jdbcParameterConsumer; + } + + private static boolean determineWhetherToForceIdSelection(int numberOfKeysToLoad, List restrictedParts) { if ( numberOfKeysToLoad > 1 ) { - forceIdentifierSelection = true; + return true; } - else { - for ( ModelPart restrictedPart : restrictedParts ) { - if ( restrictedPart instanceof ForeignKeyDescriptor ) { - forceIdentifierSelection = true; - } + + if ( restrictedParts.size() == 1 ) { + final ModelPart restrictedPart = restrictedParts.get( 0 ); + if ( Objects.equals( restrictedPart.getPartName(), NaturalIdMapping.PART_NAME ) ) { + return true; } } - EntityGraphTraversalState entityGraphTraversalState = null; + for ( ModelPart restrictedPart : restrictedParts ) { + if ( restrictedPart instanceof ForeignKeyDescriptor ) { + return true; + } + } + + return false; + } + + private static EntityGraphTraversalState determineGraphTraversalState(LoadQueryInfluencers loadQueryInfluencers) { if ( loadQueryInfluencers != null ) { final EffectiveEntityGraph effectiveEntityGraph = loadQueryInfluencers.getEffectiveEntityGraph(); if ( effectiveEntityGraph != null ) { final GraphSemantic graphSemantic = effectiveEntityGraph.getSemantic(); final RootGraphImplementor rootGraphImplementor = effectiveEntityGraph.getGraph(); if ( graphSemantic != null && rootGraphImplementor != null ) { - entityGraphTraversalState = new StandardEntityGraphTraversalStateImpl( graphSemantic, rootGraphImplementor ); + return new StandardEntityGraphTraversalStateImpl( graphSemantic, rootGraphImplementor ); } } } - this.entityGraphTraversalState = entityGraphTraversalState; - - this.lockOptions = lockOptions != null ? lockOptions : LockOptions.NONE; - this.jdbcParameterConsumer = jdbcParameterConsumer; + return null; } private LoaderSelectBuilder( diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadingEntityCollector.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadingEntityCollector.java new file mode 100644 index 0000000000..0de0fe4cc1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoadingEntityCollector.java @@ -0,0 +1,52 @@ +/* + * 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.loader.ast.internal; + +import java.util.HashSet; +import java.util.List; + +import org.hibernate.engine.spi.BatchFetchQueue; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SubselectFetch; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +/** + * Given as the target for {@link ExecutionContext#registerLoadingEntityEntry} calls when + * performing multi-loads to apply sub-select fetching support. + */ +public class LoadingEntityCollector { + + private final SubselectFetch subselectFetch; + private final BatchFetchQueue batchFetchQueue; + + LoadingEntityCollector( + EntityValuedModelPart loadingEntityDescriptor, + QuerySpec loadingSqlAst, + List jdbcParameters, + JdbcParameterBindings jdbcParameterBindings, + BatchFetchQueue batchFetchQueue) { + this.batchFetchQueue = batchFetchQueue; + this.subselectFetch = new SubselectFetch( + loadingEntityDescriptor, + loadingSqlAst, + loadingSqlAst.getFromClause().getRoots().get( 0 ), + jdbcParameters, + jdbcParameterBindings, + new HashSet<>() + ); + + } + + public void collectLoadingEntityKey(EntityKey entityKey) { + subselectFetch.getResultingEntityKeys().add( entityKey ); + batchFetchQueue.addSubselect( entityKey, subselectFetch ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdLoaderStandard.java similarity index 89% rename from hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandardImpl.java rename to hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdLoaderStandard.java index 4fe8bdfc07..ad51e89067 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdLoaderStandard.java @@ -9,7 +9,6 @@ package org.hibernate.loader.ast.internal; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -18,7 +17,6 @@ import org.hibernate.LockOptions; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.BatchFetchQueue; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.LoadQueryInfluencers; @@ -26,22 +24,19 @@ import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.Status; -import org.hibernate.engine.spi.SubselectFetch; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; import org.hibernate.event.spi.LoadEventListener; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.loader.ast.spi.MultiIdEntityLoader; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.entity.CacheEntityLoaderHelper; -import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.MultiLoadOptions; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; @@ -58,15 +53,15 @@ import org.jboss.logging.Logger; /** * @author Steve Ebersole */ -public class MultiIdEntityLoaderStandardImpl implements MultiIdEntityLoader { - private static final Logger log = Logger.getLogger( MultiIdEntityLoaderStandardImpl.class ); +public class MultiIdLoaderStandard implements MultiIdEntityLoader { + private static final Logger log = Logger.getLogger( MultiIdLoaderStandard.class ); private final EntityPersister entityDescriptor; private final SessionFactoryImplementor sessionFactory; private int idJdbcTypeCount = -1; - public MultiIdEntityLoaderStandardImpl(EntityPersister entityDescriptor, SessionFactoryImplementor sessionFactory) { + public MultiIdLoaderStandard(EntityPersister entityDescriptor, SessionFactoryImplementor sessionFactory) { this.entityDescriptor = entityDescriptor; this.sessionFactory = sessionFactory; } @@ -77,9 +72,10 @@ public class MultiIdEntityLoaderStandardImpl implements MultiIdEntityLoader load(Object[] ids, MultiLoadOptions loadOptions, SharedSessionContractImplementor session) { - // todo (6.0) : account for all of the `loadOptions`... - // for now just do a simple load + public List load(Object[] ids, MultiIdLoadOptions loadOptions, SharedSessionContractImplementor session) { + // todo (6.0) : account for all of the `loadOptions` for now just do a simple load + // ^^ atm this is handled in `MultiIdentifierLoadAccess`. Need to decide on the design we want here... + // - see `SimpleNaturalIdMultiLoadAccessImpl` for example of alternative assert ids != null; @@ -99,7 +95,7 @@ public class MultiIdEntityLoaderStandardImpl implements MultiIdEntityLoader performOrderedMultiLoad( Object[] ids, SharedSessionContractImplementor session, - MultiLoadOptions loadOptions) { + MultiIdLoadOptions loadOptions) { if ( log.isTraceEnabled() ) { log.tracef( "#performOrderedMultiLoad(`%s`, ..)", entityDescriptor.getEntityName() ); } @@ -336,38 +332,10 @@ public class MultiIdEntityLoaderStandardImpl implements MultiIdEntityLoader jdbcParameters, - JdbcParameterBindings jdbcParameterBindings, - BatchFetchQueue batchFetchQueue) { - this.batchFetchQueue = batchFetchQueue; - this.subselectFetch = new SubselectFetch( - loadingEntityDescriptor, - loadingSqlAst, - loadingSqlAst.getFromClause().getRoots().get( 0 ), - jdbcParameters, - jdbcParameterBindings, - new HashSet<>() - ); - - } - - void collectLoadingEntityKey(EntityKey entityKey) { - subselectFetch.getResultingEntityKeys().add( entityKey ); - batchFetchQueue.addSubselect( entityKey, subselectFetch ); - } - } - private List performUnorderedMultiLoad( Object[] ids, SharedSessionContractImplementor session, - MultiLoadOptions loadOptions) { + MultiIdLoadOptions loadOptions) { assert !loadOptions.isOrderReturnEnabled(); assert ids != null; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderStandard.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderStandard.java new file mode 100644 index 0000000000..f6ad59e546 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoaderStandard.java @@ -0,0 +1,93 @@ +/* + * 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.loader.ast.internal; + +import java.util.Collections; +import java.util.List; + +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.sql.results.LoadingLogger; + +/** + * Standard MultiNaturalIdLoader implementation + */ +public class MultiNaturalIdLoaderStandard implements MultiNaturalIdLoader { + + // todo (6.0) : much of the execution logic here is borrowed from `org.hibernate.loader.ast.internal.MultiIdEntityLoaderStandardImpl` + // - consider ways to consolidate/share logic + // - actually, org.hibernate.loader.ast.internal.MultiNaturalIdLoadingBatcher is pretty close + + private final EntityMappingType entityDescriptor; + + public MultiNaturalIdLoaderStandard(EntityMappingType entityDescriptor, MappingModelCreationProcess creationProcess) { + this.entityDescriptor = entityDescriptor; + } + + @Override + public List multiLoad(K[] naturalIds, MultiNaturalIdLoadOptions options, SharedSessionContractImplementor session) { + if ( naturalIds == null ) { + throw new IllegalArgumentException( "`naturalIds` is null" ); + } + + if ( naturalIds.length == 0 ) { + return Collections.emptyList(); + } + + if ( LoadingLogger.LOGGER.isTraceEnabled() ) { + LoadingLogger.LOGGER.tracef( "Starting multi natural-id loading for `%s`", entityDescriptor.getEntityName() ); + } + + final SessionFactoryImplementor sessionFactory = session.getFactory(); + + final int maxBatchSize; + if ( options.getBatchSize() != null && options.getBatchSize() > 0 ) { + maxBatchSize = options.getBatchSize(); + } + else { + maxBatchSize = session.getJdbcServices().getJdbcEnvironment().getDialect().getDefaultBatchLoadSizingStrategy().determineOptimalBatchLoadSize( + entityDescriptor.getNaturalIdMapping().getJdbcTypeCount( sessionFactory.getTypeConfiguration() ), + naturalIds.length + ); + } + + final int batchSize = Math.min( maxBatchSize, naturalIds.length ); + + final LockOptions lockOptions = (options.getLockOptions() == null) + ? new LockOptions( LockMode.NONE ) + : options.getLockOptions(); + + final MultiNaturalIdLoadingBatcher batcher = new MultiNaturalIdLoadingBatcher( + entityDescriptor, + entityDescriptor.getNaturalIdMapping(), + batchSize, + (naturalId, session1) -> { + // `naturalId` here is the one passed in by the API as part of the values array + // todo (6.0) : use this to help create the ordered results + return entityDescriptor.getNaturalIdMapping().normalizeValue( naturalId, session ); + }, + session.getLoadQueryInfluencers(), + lockOptions, + sessionFactory + ); + + final List results = batcher.multiLoad( naturalIds, options, session ); + + if ( options.isOrderReturnEnabled() ) { + throw new NotYetImplementedFor6Exception( getClass() ); + } + + return results; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java new file mode 100644 index 0000000000..8b2a5b63a8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java @@ -0,0 +1,206 @@ +/* + * 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.loader.ast.internal; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.hibernate.LockOptions; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstSelectTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl; +import org.hibernate.sql.exec.spi.Callback; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; +import org.hibernate.sql.results.graph.entity.LoadingEntityEntry; +import org.hibernate.sql.results.internal.RowTransformerPassThruImpl; +import org.hibernate.type.SerializableType; + +/** + * Batch support for natural-id multi loading + */ +public class MultiNaturalIdLoadingBatcher { + + @FunctionalInterface + interface KeyValueResolver { + /** + * Resolve the supported forms of representing the natural-id value to + * the "true" form - single value for simple natural-ids and an array for + * compound natural-ids. + * + * Generally delegates to {@link org.hibernate.metamodel.mapping.NaturalIdMapping#normalizeValue} + */ + Object resolveKeyToLoad(Object incoming, SharedSessionContractImplementor session); + } + + private final EntityMappingType entityDescriptor; + + private final SelectStatement sqlSelect; + private final List jdbcParameters; + + private final KeyValueResolver keyValueResolver; + + private final JdbcSelect jdbcSelect; + + public MultiNaturalIdLoadingBatcher( + EntityMappingType entityDescriptor, + ModelPart restrictedPart, + int batchSize, + KeyValueResolver keyValueResolver, + LoadQueryInfluencers loadQueryInfluencers, + LockOptions lockOptions, + SessionFactoryImplementor sessionFactory) { + this.entityDescriptor = entityDescriptor; + + jdbcParameters = new ArrayList<>( batchSize ); + sqlSelect = LoaderSelectBuilder.createSelect( + entityDescriptor, + // return the full entity rather than parts + null, + restrictedPart, + // no "cached" DomainResult + null, + batchSize, + loadQueryInfluencers, + lockOptions, + jdbcParameters::add, + sessionFactory + ); + + this.keyValueResolver = keyValueResolver; + + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); + final SqlAstSelectTranslator sqlAstTranslator = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ); + this.jdbcSelect = sqlAstTranslator.translate( sqlSelect ); + } + + public List multiLoad(Object[] naturalIdValues, MultiNaturalIdLoadOptions options, SharedSessionContractImplementor session) { + final ArrayList multiLoadResults = CollectionHelper.arrayList( naturalIdValues.length ); + + JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); + Iterator jdbcParamItr = jdbcParameters.iterator(); + + boolean needsExecution = false; + + for ( int i = 0; i < naturalIdValues.length; i++ ) { + final JdbcParameterBindings jdbcParamBindingsRef = jdbcParamBindings; + final Iterator jdbcParamItrRef = jdbcParamItr; + + final Object bindValue = keyValueResolver.resolveKeyToLoad( naturalIdValues[ i ], session ); + if ( bindValue != null ) { + entityDescriptor.getNaturalIdMapping().visitJdbcValues( + bindValue, + Clause.IRRELEVANT, + (jdbcValue, jdbcMapping) -> jdbcParamBindingsRef.addBinding( + jdbcParamItrRef.next(), + new JdbcParameterBindingImpl( jdbcMapping, jdbcValue ) + ), + session + ); + needsExecution = true; + } + + if ( ! jdbcParamItr.hasNext() ) { + // we've hit the batch mark + final List batchResults = performLoad( jdbcParamBindings, session ); + multiLoadResults.addAll( batchResults ); + + jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); + jdbcParamItr = jdbcParameters.iterator(); + + needsExecution = false; + } + } + + if ( needsExecution ) { + while ( jdbcParamItr.hasNext() ) { + // pad the remaining parameters with null + jdbcParamBindings.addBinding( + jdbcParamItr.next(), + new JdbcParameterBindingImpl( SerializableType.INSTANCE, null ) + ); + } + final List batchResults = performLoad( jdbcParamBindings, session ); + multiLoadResults.addAll( batchResults ); + } + + return multiLoadResults; + } + + private List performLoad(JdbcParameterBindings jdbcParamBindings, SharedSessionContractImplementor session) { + final LoadingEntityCollector loadingEntityCollector; + + if ( entityDescriptor.getEntityPersister().hasSubselectLoadableCollections() ) { + loadingEntityCollector = new LoadingEntityCollector( + entityDescriptor, + sqlSelect.getQuerySpec(), + jdbcParameters, + jdbcParamBindings, + session.getPersistenceContext().getBatchFetchQueue() + ); + } + else { + loadingEntityCollector = null; + } + + return JdbcSelectExecutorStandardImpl.INSTANCE.list( + jdbcSelect, + jdbcParamBindings, + new ExecutionContext() { + @Override + public SharedSessionContractImplementor getSession() { + return session; + } + + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + + @Override + public QueryParameterBindings getQueryParameterBindings() { + return QueryParameterBindings.NO_PARAM_BINDINGS; + } + + @Override + public Callback getCallback() { + return null; + } + + @Override + public void registerLoadingEntityEntry(EntityKey entityKey, LoadingEntityEntry entry) { + if ( loadingEntityCollector != null ) { + loadingEntityCollector.collectLoadingEntityKey( entityKey ); + } + } + }, + RowTransformerPassThruImpl.instance(), + true + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java new file mode 100644 index 0000000000..c7ead1a6f5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java @@ -0,0 +1,262 @@ +/* + * 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.loader.ast.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.hibernate.HibernateException; +import org.hibernate.LockOptions; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.NaturalIdPostLoadListener; +import org.hibernate.loader.NaturalIdPreLoadListener; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.spi.Callback; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; + +/** + * NaturalIdLoader for simple natural-ids + */ +public class SimpleNaturalIdLoader extends AbstractNaturalIdLoader { + + public SimpleNaturalIdLoader( + SimpleNaturalIdMapping naturalIdMapping, + NaturalIdPreLoadListener preLoadListener, + NaturalIdPostLoadListener postLoadListener, + EntityMappingType entityDescriptor, + MappingModelCreationProcess creationProcess) { + super( naturalIdMapping, preLoadListener, postLoadListener, entityDescriptor, creationProcess ); + } + + @Override + protected SimpleNaturalIdMapping naturalIdMapping() { + return (SimpleNaturalIdMapping) super.naturalIdMapping(); + } + + @Override + protected void applyNaturalIdAsJdbcParameters( + Object naturalIdToLoad, + List jdbcParameters, + JdbcParameterBindings jdbcParamBindings, + SharedSessionContractImplementor session) { + assert jdbcParameters.size() == 1; + + final Object bindableValue = naturalIdMapping().normalizeValue( naturalIdToLoad, session ); + + final SingularAttributeMapping attributeMapping = naturalIdMapping().getNaturalIdAttributes().get( 0 ); + attributeMapping.visitJdbcValues( + bindableValue, + Clause.WHERE, + (jdbcValue, jdbcMapping) -> { + final JdbcParameter jdbcParam = jdbcParameters.get( 0 ); + jdbcParamBindings.addBinding( + jdbcParam, + new JdbcParameterBindingImpl( jdbcMapping, jdbcValue ) + ); + }, + session + ); + } + + @Override + protected Object resolveNaturalIdBindValue(Object naturalIdToLoad, SharedSessionContractImplementor session) { + return naturalIdMapping().normalizeValue( naturalIdToLoad, session ); + } + + @Override + public Object resolveIdToNaturalId(Object id, SharedSessionContractImplementor session) { + final SessionFactoryImplementor sessionFactory = session.getFactory(); + + final List jdbcParameters = new ArrayList<>(); + final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( + entityDescriptor(), + naturalIdMapping().getNaturalIdAttributes(), + entityDescriptor().getIdentifierMapping(), + null, + 1, + session.getLoadQueryInfluencers(), + LockOptions.READ, + jdbcParameters::add, + sessionFactory + ); + + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); + + final JdbcSelect jdbcSelect = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ).translate( sqlSelect ); + + final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); + final Iterator jdbcParamItr = jdbcParameters.iterator(); + + entityDescriptor().getIdentifierMapping().visitJdbcValues( + id, + Clause.WHERE, + (value, type) -> { + assert jdbcParamItr.hasNext(); + final JdbcParameter jdbcParam = jdbcParamItr.next(); + jdbcParamBindings.addBinding( + jdbcParam, + new JdbcParameterBindingImpl( type, value ) + ); + }, + session + ); + + + final List results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( + jdbcSelect, + jdbcParamBindings, + new ExecutionContext() { + @Override + public SharedSessionContractImplementor getSession() { + return session; + } + + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + + @Override + public QueryParameterBindings getQueryParameterBindings() { + return QueryParameterBindings.NO_PARAM_BINDINGS; + } + + @Override + public Callback getCallback() { + return afterLoadAction -> { + }; + } + }, + row -> row, + true + ); + + if ( results.size() > 1 ) { + throw new HibernateException( + String.format( + "Resolving id to natural-id returned more that one row : %s #%s", + entityDescriptor().getEntityName(), + id + ) + ); + } + + return results.get( 0 ); + } + + @Override + protected boolean isSimple() { + return true; + } + + @Override + public Object resolveNaturalIdToId( + Object naturalIdValue, + SharedSessionContractImplementor session) { + final Object bindValue = naturalIdMapping().normalizeValue( naturalIdValue, session ); + + final SessionFactoryImplementor sessionFactory = session.getFactory(); + final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); + final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); + final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); + + final List jdbcParameters = new ArrayList<>(); + final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect( + entityDescriptor(), + Collections.singletonList( entityDescriptor().getIdentifierMapping() ), + naturalIdMapping(), + null, + 1, + session.getLoadQueryInfluencers(), + LockOptions.READ, + jdbcParameters::add, + sessionFactory + ); + assert jdbcParameters.size() == 1; + + final JdbcSelect jdbcSelect = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ).translate( sqlSelect ); + + final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() ); + final Iterator jdbcParamItr = jdbcParameters.iterator(); + + final SingularAttributeMapping attributeMapping = naturalIdMapping().getAttribute(); + attributeMapping.visitJdbcValues( + bindValue, + Clause.WHERE, + (jdbcValue, jdbcMapping) -> { + assert jdbcParamItr.hasNext(); + jdbcParamBindings.addBinding( + jdbcParamItr.next(), + new JdbcParameterBindingImpl( jdbcMapping, jdbcValue ) + ); + }, + session + ); + + + final List results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list( + jdbcSelect, + jdbcParamBindings, + new ExecutionContext() { + @Override + public SharedSessionContractImplementor getSession() { + return session; + } + + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + + @Override + public QueryParameterBindings getQueryParameterBindings() { + return QueryParameterBindings.NO_PARAM_BINDINGS; + } + + @Override + public Callback getCallback() { + return afterLoadAction -> { + }; + } + }, + row -> row[0], + true + ); + + if ( results.size() > 1 ) { + throw new HibernateException( + String.format( + "Resolving natural-id to id returned more that one row : %s [%s]", + entityDescriptor().getEntityName(), + bindValue + ) + ); + } + + return results.get( 0 ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java index e14c6a0438..215a88f58e 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdEntityLoader.java @@ -10,7 +10,6 @@ import java.util.List; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.MultiLoadOptions; /** * Loader subtype for loading multiple entities by multiple identifier values. @@ -21,5 +20,5 @@ public interface MultiIdEntityLoader extends Loader { @Override EntityPersister getLoadable(); - List load(Object[] ids, MultiLoadOptions options, SharedSessionContractImplementor session); + List load(Object[] ids, MultiIdLoadOptions options, SharedSessionContractImplementor session); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java new file mode 100644 index 0000000000..149a211b52 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java @@ -0,0 +1,28 @@ +/* + * 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.loader.ast.spi; + +/** + * Encapsulation of the options for loading multiple entities by id + */ +public interface MultiIdLoadOptions extends MultiLoadOptions { + /** + * Check the first-level cache first, and only if the entity is not found in the cache + * should Hibernate hit the database. + * + * @return the session cache is checked first + */ + boolean isSessionCheckingEnabled(); + + /** + * Check the second-level cache first, and only if the entity is not found in the cache + * should Hibernate hit the database. + * + * @return the session factory cache is checked first + */ + boolean isSecondLevelCacheCheckingEnabled(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/MultiLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java similarity index 55% rename from hibernate-core/src/main/java/org/hibernate/persister/entity/MultiLoadOptions.java rename to hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java index f93b4332ed..94ccf5f9fb 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/MultiLoadOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java @@ -1,35 +1,17 @@ /* * 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 . + * 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.persister.entity; +package org.hibernate.loader.ast.spi; import org.hibernate.LockOptions; /** - * Encapsulation of the options for performing a load by multiple identifiers. - * - * @author Steve Ebersole + * Base contract for options for multi-load operations */ public interface MultiLoadOptions { - /** - * Check the first-level cache first, and only if the entity is not found in the cache - * should Hibernate hit the database. - * - * @return the session cache is checked first - */ - boolean isSessionCheckingEnabled(); - - /** - * Check the second-level cache first, and only if the entity is not found in the cache - * should Hibernate hit the database. - * - * @return the session factory cache is checked first - */ - boolean isSecondLevelCacheCheckingEnabled(); - /** * Should we returned entities that are scheduled for deletion. * diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java new file mode 100644 index 0000000000..d200d08fe8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoadOptions.java @@ -0,0 +1,13 @@ +/* + * 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.loader.ast.spi; + +/** + * Encapsulation of the options for loading multiple entities by natural-id + */ +public interface MultiNaturalIdLoadOptions extends MultiLoadOptions { +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java new file mode 100644 index 0000000000..3129fc619c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiNaturalIdLoader.java @@ -0,0 +1,33 @@ +/* + * 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.loader.ast.spi; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; + +/** + * Loader for entities by multiple natural-ids + * + * @param The entity Java type + */ +public interface MultiNaturalIdLoader { + /** + * Load multiple entities by natural-id. The exact result depends on the passed options. + * + * @param naturalIds The natural-ids to load. The values of this array will depend on whether the + * natural-id is simple or complex. + * + * @param The basic form for a natural-id is a Map of its attribute values, or an array of the + * values positioned according to "attribute ordering". Simple natural-ids can also be expressed + * by their simple (basic/embedded) type. + */ + List multiLoad(K[] naturalIds, MultiNaturalIdLoadOptions options, SharedSessionContractImplementor session); +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java index 738dac56aa..fa4abb37d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/NaturalIdLoader.java @@ -6,6 +6,8 @@ */ package org.hibernate.loader.ast.spi; +import java.util.List; + import org.hibernate.LockOptions; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -15,7 +17,25 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; * @author Steve Ebersole */ public interface NaturalIdLoader extends Loader { + /** + * Options for the {@link #load} method + */ interface LoadOptions { + /** + * Singleton access + */ + LoadOptions NONE = new LoadOptions() { + @Override + public LockOptions getLockOptions() { + return LockOptions.READ; + } + + @Override + public boolean isSynchronizationEnabled() { + return false; + } + }; + /** * The locking options for the loaded entity */ @@ -44,9 +64,12 @@ public interface NaturalIdLoader extends Loader { T load(Object naturalIdToLoad, LoadOptions options, SharedSessionContractImplementor session); /** - * Resolve the natural-id value from an id + * Resolve the id from natural-id value */ - Object[] resolveIdToNaturalId(Object id, SharedSessionContractImplementor session); + Object resolveNaturalIdToId(Object naturalIdValue, SharedSessionContractImplementor session); - Object resolveNaturalIdToId(Object[] naturalIdValues, SharedSessionContractImplementor session); + /** + * Resolve the natural-id value(s) from an id + */ + Object resolveIdToNaturalId(Object id, SharedSessionContractImplementor session); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/entity/CacheEntityLoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/entity/CacheEntityLoaderHelper.java index 4a24dea946..9513071847 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/entity/CacheEntityLoaderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/entity/CacheEntityLoaderHelper.java @@ -7,7 +7,9 @@ package org.hibernate.loader.entity; import org.hibernate.HibernateException; +import org.hibernate.Incubating; import org.hibernate.LockMode; +import org.hibernate.LockOptions; import org.hibernate.WrongClassException; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.entry.CacheEntry; @@ -23,18 +25,14 @@ import org.hibernate.engine.spi.ManagedEntity; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.Status; -import org.hibernate.event.internal.AbstractLockUpgradeEventListener; -import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventSource; -import org.hibernate.event.spi.EventType; import org.hibernate.event.spi.LoadEvent; import org.hibernate.event.spi.LoadEventListener; import org.hibernate.event.spi.PostLoadEvent; -import org.hibernate.event.spi.PostLoadEventListener; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.FastSessionServices; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; import org.hibernate.proxy.HibernateProxy; @@ -44,10 +42,12 @@ import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.Type; import org.hibernate.type.TypeHelper; +import static org.hibernate.loader.ast.internal.LoaderHelper.upgradeLock; + /** * @author Vlad Mihalcea */ -public class CacheEntityLoaderHelper extends AbstractLockUpgradeEventListener { +public class CacheEntityLoaderHelper { public static final CacheEntityLoaderHelper INSTANCE = new CacheEntityLoaderHelper(); @@ -62,6 +62,46 @@ public class CacheEntityLoaderHelper extends AbstractLockUpgradeEventListener { private CacheEntityLoaderHelper() { } + @Incubating + public PersistenceContextEntry loadFromSessionCache( + EntityKey keyToLoad, + LoadEventListener.LoadType options, + LockOptions lockOptions, + SharedSessionContractImplementor session) { + final Object old = session.getEntityUsingInterceptor( keyToLoad ); + + if ( old != null ) { + // this object was already loaded + EntityEntry oldEntry = session.getPersistenceContext().getEntry( old ); + if ( options.isCheckDeleted() ) { + Status status = oldEntry.getStatus(); + if ( status == Status.DELETED || status == Status.GONE ) { + LoadingLogger.LOGGER.debug( + "Load request found matching entity in context, but it is scheduled for removal; returning null" ); + return new PersistenceContextEntry( old, EntityStatus.REMOVED_ENTITY_MARKER ); + } + } + if ( options.isAllowNulls() ) { + final EntityPersister persister = session.getFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( keyToLoad.getEntityName() ); + if ( ! persister.isInstance( old ) ) { + LOG.debugf( + "Load request found matching entity in context, but the matched entity was of an inconsistent return type. " + + "Setting status as `%s`", + EntityStatus.INCONSISTENT_RTN_CLASS_MARKER + ); + return new PersistenceContextEntry( old, EntityStatus.INCONSISTENT_RTN_CLASS_MARKER ); + } + } + + upgradeLock( old, oldEntry, lockOptions, session ); + } + + return new PersistenceContextEntry( old, EntityStatus.MANAGED ); + } + /** * Attempts to locate the entity in the session-level cache. *

diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java index efe50e96fa..1b301c5322 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/NaturalIdMapping.java @@ -8,9 +8,28 @@ package org.hibernate.metamodel.mapping; import java.util.List; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; +import org.hibernate.loader.ast.spi.NaturalIdLoader; + /** - * @author Steve Ebersole + * Mapping for an entity's natural-id, if one is defined */ public interface NaturalIdMapping extends VirtualModelPart { + String PART_NAME = "{natural-id}"; + + /** + * The attribute(s) making up the natural-id. + */ List getNaturalIdAttributes(); + + @Override + default String getPartName() { + return PART_NAME; + } + + NaturalIdLoader getNaturalIdLoader(); + MultiNaturalIdLoader getMultiNaturalIdLoader(); + + Object normalizeValue(Object incoming, SharedSessionContractImplementor session); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java new file mode 100644 index 0000000000..916b356e30 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractNaturalIdMapping.java @@ -0,0 +1,38 @@ +/* + * 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.metamodel.mapping.internal; + +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.metamodel.model.domain.NavigableRole; + +/** + * @author Steve Ebersole + */ +public abstract class AbstractNaturalIdMapping implements NaturalIdMapping { + private final EntityMappingType declaringType; + private final String cacheRegionName; + + private final NavigableRole role; + + public AbstractNaturalIdMapping(EntityMappingType declaringType, String cacheRegionName) { + this.declaringType = declaringType; + this.cacheRegionName = cacheRegionName; + + this.role = declaringType.getNavigableRole().append( PART_NAME ); + } + + @Override + public NavigableRole getNavigableRole() { + return role; + } + + @Override + public EntityMappingType findContainingEntityMapping() { + return declaringType; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java new file mode 100644 index 0000000000..6ba1e50df1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java @@ -0,0 +1,216 @@ +/* + * 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.metamodel.mapping.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.NaturalIdPostLoadListener; +import org.hibernate.loader.NaturalIdPreLoadListener; +import org.hibernate.loader.ast.internal.CompoundNaturalIdLoader; +import org.hibernate.loader.ast.internal.MultiNaturalIdLoaderStandard; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; +import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.metamodel.mapping.ColumnConsumer; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.query.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Multi-attribute NaturalIdMapping implementation + */ +public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implements MappingType { + + // todo (6.0) : create a composite MappingType for this descriptor's Object[]? + + private final List attributes; + private final List jdbcMappings; + + private final NaturalIdLoader loader; + private final MultiNaturalIdLoader multiLoader; + + public CompoundNaturalIdMapping( + EntityMappingType declaringType, + List attributes, + String cacheRegionName, + MappingModelCreationProcess creationProcess) { + super( declaringType, cacheRegionName ); + this.attributes = attributes; + + final List jdbcMappings = new ArrayList<>(); + final TypeConfiguration typeConfiguration = creationProcess.getCreationContext().getTypeConfiguration(); + attributes.forEach( + (attribute) -> attribute.visitJdbcTypes( jdbcMappings::add, Clause.IRRELEVANT, typeConfiguration ) + ); + this.jdbcMappings = jdbcMappings; + + loader = new CompoundNaturalIdLoader<>( + this, + NaturalIdPreLoadListener.NO_OP, + NaturalIdPostLoadListener.NO_OP, + declaringType, + creationProcess + ); + multiLoader = new MultiNaturalIdLoaderStandard<>( declaringType, creationProcess ); + } + + @Override + @SuppressWarnings( "rawtypes" ) + public Object normalizeValue(Object incoming, SharedSessionContractImplementor session) { + if ( incoming instanceof Object[] ) { + return incoming; + } + + if ( incoming instanceof Map ) { + final Map valueMap = (Map) incoming; + final List attributes = getNaturalIdAttributes(); + final Object[] values = new Object[ attributes.size() ]; + for ( int i = 0; i < attributes.size(); i++ ) { + values[ i ] = valueMap.get( attributes.get( i ).getAttributeName() ); + } + return values; + } + + throw new UnsupportedOperationException( "Do not know how to normalize compound natural-id value : " + incoming ); + } + + @Override + public List getNaturalIdAttributes() { + return attributes; + } + + @Override + public NaturalIdLoader getNaturalIdLoader() { + return loader; + } + + @Override + public MultiNaturalIdLoader getMultiNaturalIdLoader() { + return multiLoader; + } + + @Override + public MappingType getPartMappingType() { + return this; + } + + @Override + public JavaTypeDescriptor getJavaTypeDescriptor() { + throw new NotYetImplementedFor6Exception( getClass() ); + } + + @Override + public JavaTypeDescriptor getMappedJavaTypeDescriptor() { + return getJavaTypeDescriptor(); + } + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // ModelPart + + @Override + public DomainResult createDomainResult(NavigablePath navigablePath, TableGroup tableGroup, String resultVariable, DomainResultCreationState creationState) { + throw new NotYetImplementedFor6Exception( getClass() ); + } + + @Override + public void applySqlSelections(NavigablePath navigablePath, TableGroup tableGroup, DomainResultCreationState creationState) { + attributes.forEach( + (attribute) -> attribute.applySqlSelections( navigablePath, tableGroup, creationState ) + ); + } + + @Override + public void applySqlSelections(NavigablePath navigablePath, TableGroup tableGroup, DomainResultCreationState creationState, BiConsumer selectionConsumer) { + attributes.forEach( + (attribute) -> attribute.applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ) + ); + } + + @Override + public void visitColumns(ColumnConsumer consumer) { + attributes.forEach( + (attribute) -> attribute.visitColumns( consumer ) + ); + } + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Bindable + + @Override + public int getJdbcTypeCount(TypeConfiguration typeConfiguration) { + return jdbcMappings.size(); + } + + @Override + public List getJdbcMappings(TypeConfiguration typeConfiguration) { + return jdbcMappings; + } + + @Override + public void visitJdbcTypes(Consumer action, Clause clause, TypeConfiguration typeConfiguration) { + jdbcMappings.forEach( action ); + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + assert value instanceof Object[]; + + final Object[] incoming = (Object[]) value; + assert incoming.length == attributes.size(); + + final Object[] outgoing = new Object[ incoming.length ]; + + for ( int i = 0; i < attributes.size(); i++ ) { + final SingularAttributeMapping attribute = attributes.get( i ); + outgoing[ i ] = attribute.disassemble( incoming[ i ], session ); + } + + return outgoing; + } + + @Override + public void visitDisassembledJdbcValues(Object value, Clause clause, JdbcValuesConsumer valuesConsumer, SharedSessionContractImplementor session) { + assert value instanceof Object[]; + + final Object[] incoming = (Object[]) value; + assert incoming.length == attributes.size(); + + for ( int i = 0; i < attributes.size(); i++ ) { + final SingularAttributeMapping attribute = attributes.get( i ); + attribute.visitDisassembledJdbcValues( incoming[ i ], clause, valuesConsumer, session ); + } + } + + @Override + public void visitJdbcValues(Object value, Clause clause, JdbcValuesConsumer valuesConsumer, SharedSessionContractImplementor session) { + assert value instanceof Object[]; + + final Object[] incoming = (Object[]) value; + assert incoming.length == attributes.size(); + + for ( int i = 0; i < attributes.size(); i++ ) { + final SingularAttributeMapping attribute = attributes.get( i ); + attribute.visitJdbcValues( incoming[ i ], clause, valuesConsumer, session ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java new file mode 100644 index 0000000000..0e31a403e6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SimpleNaturalIdMapping.java @@ -0,0 +1,172 @@ +/* + * 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.metamodel.mapping.internal; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.NaturalIdPostLoadListener; +import org.hibernate.loader.NaturalIdPreLoadListener; +import org.hibernate.loader.ast.internal.MultiNaturalIdLoaderStandard; +import org.hibernate.loader.ast.internal.SimpleNaturalIdLoader; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; +import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.metamodel.mapping.ColumnConsumer; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.query.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Single-attribute NaturalIdMapping implementation + */ +public class SimpleNaturalIdMapping extends AbstractNaturalIdMapping { + private final SingularAttributeMapping attribute; + + private final SimpleNaturalIdLoader loader; + private final MultiNaturalIdLoader multiLoader; + + public SimpleNaturalIdMapping( + SingularAttributeMapping attribute, + EntityMappingType declaringType, + String cacheRegionName, + MappingModelCreationProcess creationProcess) { + super( declaringType, cacheRegionName ); + this.attribute = attribute; + + this.loader = new SimpleNaturalIdLoader<>( + this, + NaturalIdPreLoadListener.NO_OP, + NaturalIdPostLoadListener.NO_OP, + declaringType, + creationProcess + ); + this.multiLoader = new MultiNaturalIdLoaderStandard<>( declaringType, creationProcess ); + } + + @Override + public Object normalizeValue(Object incoming, SharedSessionContractImplementor session) { + return normalizeValue( incoming ); + } + + @SuppressWarnings( "rawtypes" ) + public Object normalizeValue(Object naturalIdToLoad) { + if ( naturalIdToLoad instanceof Map ) { + final Map valueMap = (Map) naturalIdToLoad; + assert valueMap.size() == 1; + assert valueMap.containsKey( getAttribute().getAttributeName() ); + return valueMap.get( getAttribute().getAttributeName() ); + } + + if ( naturalIdToLoad instanceof Object[] ) { + final Object[] values = (Object[]) naturalIdToLoad; + assert values.length == 1; + return values[0]; + } + + return naturalIdToLoad; + } + + public SingularAttributeMapping getAttribute() { + return attribute; + } + + @Override + public NaturalIdLoader getNaturalIdLoader() { + return loader; + } + + @Override + public MultiNaturalIdLoader getMultiNaturalIdLoader() { + return multiLoader; + } + + @Override + public List getNaturalIdAttributes() { + return Collections.singletonList( attribute ); + } + + @Override + public MappingType getPartMappingType() { + return attribute.getPartMappingType(); + } + + @Override + public JavaTypeDescriptor getJavaTypeDescriptor() { + return attribute.getJavaTypeDescriptor(); + } + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + return attribute.createDomainResult( navigablePath, tableGroup, resultVariable, creationState ); + } + + @Override + public void applySqlSelections(NavigablePath navigablePath, TableGroup tableGroup, DomainResultCreationState creationState) { + attribute.applySqlSelections( navigablePath, tableGroup, creationState ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + attribute.applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); + } + + @Override + public void visitColumns(ColumnConsumer consumer) { + attribute.visitColumns( consumer ); + } + + @Override + public int getJdbcTypeCount(TypeConfiguration typeConfiguration) { + return attribute.getJdbcTypeCount( typeConfiguration ); + } + + @Override + public List getJdbcMappings(TypeConfiguration typeConfiguration) { + return attribute.getJdbcMappings( typeConfiguration ); + } + + @Override + public void visitJdbcTypes(Consumer action, Clause clause, TypeConfiguration typeConfiguration) { + attribute.visitJdbcTypes( action, clause, typeConfiguration ); + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + return attribute.disassemble( value, session ); + } + + @Override + public void visitDisassembledJdbcValues(Object value, Clause clause, JdbcValuesConsumer valuesConsumer, SharedSessionContractImplementor session) { + attribute.visitDisassembledJdbcValues( value, clause, valuesConsumer, session ); + } + + @Override + public void visitJdbcValues(Object value, Clause clause, JdbcValuesConsumer valuesConsumer, SharedSessionContractImplementor session) { + attribute.visitJdbcValues( value, clause, valuesConsumer, session ); + } +} 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 6f7103fb96..cb440bed95 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 @@ -78,8 +78,6 @@ import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.CachedNaturalIdValueSource; import org.hibernate.engine.spi.CascadeStyle; -import org.hibernate.engine.spi.CascadingAction; -import org.hibernate.engine.spi.CascadingActions; import org.hibernate.engine.spi.CollectionKey; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityEntryFactory; @@ -112,8 +110,7 @@ import org.hibernate.internal.util.collections.LockModeEnumMap; import org.hibernate.jdbc.Expectation; import org.hibernate.jdbc.Expectations; import org.hibernate.jdbc.TooManyRowsAffectedException; -import org.hibernate.loader.ast.internal.MultiIdEntityLoaderStandardImpl; -import org.hibernate.loader.ast.internal.NaturalIdLoaderStandardImpl; +import org.hibernate.loader.ast.internal.MultiIdLoaderStandard; import org.hibernate.loader.ast.internal.Preparable; import org.hibernate.loader.ast.internal.SingleIdEntityLoaderDynamicBatch; import org.hibernate.loader.ast.internal.SingleIdEntityLoaderProvidedQueryImpl; @@ -121,6 +118,7 @@ import org.hibernate.loader.ast.internal.SingleIdEntityLoaderStandardImpl; import org.hibernate.loader.ast.internal.SingleUniqueKeyEntityLoaderStandard; import org.hibernate.loader.ast.spi.Loader; import org.hibernate.loader.ast.spi.MultiIdEntityLoader; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.ast.spi.NaturalIdLoader; import org.hibernate.loader.ast.spi.SingleIdEntityLoader; import org.hibernate.loader.ast.spi.SingleUniqueKeyEntityLoader; @@ -155,14 +153,16 @@ import org.hibernate.metamodel.mapping.Queryable; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.StateArrayContributorMapping; import org.hibernate.metamodel.mapping.StateArrayContributorMetadata; -import org.hibernate.metamodel.mapping.internal.DiscriminatedAssociationAttributeMapping; import org.hibernate.metamodel.mapping.internal.BasicEntityIdentifierMappingImpl; +import org.hibernate.metamodel.mapping.internal.CompoundNaturalIdMapping; +import org.hibernate.metamodel.mapping.internal.DiscriminatedAssociationAttributeMapping; import org.hibernate.metamodel.mapping.internal.EntityDiscriminatorMappingImpl; import org.hibernate.metamodel.mapping.internal.EntityRowIdMappingImpl; import org.hibernate.metamodel.mapping.internal.EntityVersionMappingImpl; import org.hibernate.metamodel.mapping.internal.InFlightEntityMappingType; import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; @@ -251,7 +251,6 @@ public abstract class AbstractEntityPersister private final SingleIdEntityLoader singleIdEntityLoader; private final MultiIdEntityLoader multiIdEntityLoader; - private final NaturalIdLoader naturalIdLoader; private SqmMultiTableMutationStrategy sqmMultiTableMutationStrategy; @@ -749,11 +748,7 @@ public abstract class AbstractEntityPersister // to load at once. i.e. it limits the number of the generated IN-list JDBC-parameters in a given // PreparedStatement, opting to split the load into multiple JDBC operations to work around database // limits on number of parameters, number of IN-list values, etc - multiIdEntityLoader = new MultiIdEntityLoaderStandardImpl( this, factory ); - - naturalIdLoader = bootDescriptor.hasNaturalId() - ? new NaturalIdLoaderStandardImpl( this ) - : null; + multiIdEntityLoader = new MultiIdLoaderStandard( this, factory ); Iterator iter = bootDescriptor.getIdentifier().getColumnIterator(); int i = 0; @@ -4443,7 +4438,6 @@ public abstract class AbstractEntityPersister prepareLoader( singleIdEntityLoader ); prepareLoader( multiIdEntityLoader ); - prepareLoader( naturalIdLoader ); doPostInstantiate(); } @@ -4563,7 +4557,7 @@ public abstract class AbstractEntityPersister } @Override - public List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiLoadOptions loadOptions) { + public List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiIdLoadOptions loadOptions) { return multiIdEntityLoader.load( ids, loadOptions, session ); } @@ -5575,7 +5569,13 @@ public abstract class AbstractEntityPersister ); } - return naturalIdLoader.resolveIdToNaturalId( id, session ); + final Object result = naturalIdMapping.getNaturalIdLoader().resolveIdToNaturalId( id, session ); + if ( result instanceof Object[] ) { + return (Object[]) result; + } + else { + return new Object[] { result }; + } } private void verifyHasNaturalId() { @@ -5584,6 +5584,12 @@ public abstract class AbstractEntityPersister } } + @Override + public NaturalIdLoader getNaturalIdLoader() { + verifyHasNaturalId(); + return naturalIdMapping.getNaturalIdLoader(); + } + @Override public Object loadEntityIdByNaturalId( Object[] naturalIdValues, @@ -5599,7 +5605,7 @@ public abstract class AbstractEntityPersister ); } - return naturalIdLoader.resolveNaturalIdToId( naturalIdValues, session ); + return naturalIdMapping.getNaturalIdLoader().resolveNaturalIdToId( naturalIdValues, session ); } public boolean hasNaturalIdentifier() { @@ -5842,6 +5848,8 @@ public abstract class AbstractEntityPersister getAttributeMappings(); + postProcessAttributeMappings( creationProcess, bootEntityDescriptor ); + final ReflectionOptimizer reflectionOptimizer = representationStrategy.getReflectionOptimizer(); if ( reflectionOptimizer != null ) { @@ -5893,12 +5901,61 @@ public abstract class AbstractEntityPersister (role, process) -> new EntityRowIdMappingImpl( rowIdName, this.getTableName(), this) ); } - // todo (6.0) : support for natural-id not yet implemented - naturalIdMapping = null; discriminatorMapping = generateDiscriminatorMapping(); } + private void postProcessAttributeMappings(MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { + if ( bootEntityDescriptor.hasNaturalId() ) { + naturalIdMapping = generateNaturalIdMapping( creationProcess, bootEntityDescriptor ); + } + else { + naturalIdMapping = null; + } + } + + private NaturalIdMapping generateNaturalIdMapping(MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { + assert bootEntityDescriptor.hasNaturalId(); + + final List naturalIdAttributes = new ArrayList<>(); + final Iterator iterator = bootEntityDescriptor.getPropertyIterator(); + iterator.forEachRemaining( + property -> { + if ( property.isNaturalIdentifier() ) { + final AttributeMapping attributeMapping = findAttributeMapping( property.getName() ); + if ( attributeMapping instanceof SingularAttributeMapping ) { + naturalIdAttributes.add( (SingularAttributeMapping) attributeMapping ); + } + else { + throw new MappingException( "Natural-id only valid for singular attributes : " + property.getName() ); + } + } + } + ); + + if ( naturalIdAttributes.isEmpty() ) { + throw new MappingException( "Could not locate natural-id attribute(s)" ); + } + + if ( naturalIdAttributes.size() == 1 ) { + return new SimpleNaturalIdMapping( + naturalIdAttributes.get( 0 ), + this, + bootEntityDescriptor.getNaturalIdCacheRegionName(), + creationProcess + ); + } + else { + return new CompoundNaturalIdMapping( + this, + naturalIdAttributes, + bootEntityDescriptor.getNaturalIdCacheRegionName(), + creationProcess + ); + } + + } + protected static SqmMultiTableMutationStrategy interpretSqmMultiTableStrategy( AbstractEntityPersister entityMappingDescriptor, MappingModelCreationProcess creationProcess) { 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 1cc77f3fe1..b0ff9f115b 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 @@ -32,6 +32,8 @@ import org.hibernate.id.IdentifierGenerator; import org.hibernate.internal.FilterAliasGenerator; import org.hibernate.internal.TableGroupFilterAliasGenerator; import org.hibernate.loader.ast.spi.Loadable; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; +import org.hibernate.loader.ast.spi.NaturalIdLoader; import org.hibernate.metadata.ClassMetadata; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.InFlightEntityMappingType; @@ -409,6 +411,12 @@ public interface EntityPersister */ boolean hasLazyProperties(); + default NaturalIdLoader getNaturalIdLoader() { + throw new UnsupportedOperationException( + "EntityPersister implementation `" + getClass().getName() + "` does not support `#getNaturalIdLoader`" + ); + } + /** * Load the id for the entity based on the natural id. * @return @@ -447,7 +455,7 @@ public interface EntityPersister * * @return The loaded, matching entities */ - List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiLoadOptions loadOptions); + List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiIdLoadOptions loadOptions); /** * Do a version check (optional operation) diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/ejb3configuration/PersisterClassProviderTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/ejb3configuration/PersisterClassProviderTest.java index a73a4f4fac..2fbe138574 100644 --- a/hibernate-core/src/test/java/org/hibernate/jpa/test/ejb3configuration/PersisterClassProviderTest.java +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/ejb3configuration/PersisterClassProviderTest.java @@ -49,12 +49,11 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.metamodel.mapping.EntityVersionMapping; import org.hibernate.metamodel.mapping.NaturalIdMapping; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.MultiLoadOptions; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.persister.internal.PersisterClassResolverInitiator; import org.hibernate.persister.spi.PersisterClassResolver; import org.hibernate.persister.spi.PersisterCreationContext; @@ -325,7 +324,7 @@ public class PersisterClassProviderTest { } @Override - public List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiLoadOptions loadOptions) { + public List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiIdLoadOptions loadOptions) { return Collections.emptyList(); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/mapping/naturalid/CompoundNaturalIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/mapping/naturalid/CompoundNaturalIdTests.java new file mode 100644 index 0000000000..83fdb381b4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/mapping/naturalid/CompoundNaturalIdTests.java @@ -0,0 +1,266 @@ +/* + * 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.metamodel.mapping.naturalid; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.NaturalIdLoadAccess; +import org.hibernate.NaturalIdMultiLoadAccess; +import org.hibernate.annotations.NaturalId; +import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.MappingMetamodel; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests for composite (multiple attributes) natural-ids + */ +@DomainModel( annotatedClasses = CompoundNaturalIdTests.Account.class ) +@SessionFactory +public class CompoundNaturalIdTests { + public static final Object[] VALUE_ARRAY = new Object[] { "matrix", "neo" }; + public static final Map VALUE_NAP = toMap( "system", "matrix", "username", "neo" ); + + private static Map toMap(String... values) { + assert values.length % 2 == 0; + + final HashMap valuesMap = new HashMap<>(); + for ( int i = 0; i < values.length; i += 2 ) { + valuesMap.put( values[i], values[i+1] ); + } + + return valuesMap; + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new Account( 1, "neo", "matrix", "neo@nebuchadnezzar.zion.net" ) ); + session.persist( new Account( 2, "trinity", "matrix", "trin@nebuchadnezzar.zion.net" ) ); + } + ); + } + + @AfterEach + public void releaseTestData(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete Account" ).executeUpdate(); + } + ); + } + + @Test + public void testProcessing(DomainModelScope domainModelScope, SessionFactoryScope factoryScope) { + final PersistentClass accountBootMapping = domainModelScope.getDomainModel().getEntityBinding( Account.class.getName() ); + assertThat( accountBootMapping.hasNaturalId(), is( true ) ); + final Property username = accountBootMapping.getProperty( "username" ); + assertThat( username.isNaturalIdentifier(), is( true ) ); + final Property system = accountBootMapping.getProperty( "system" ); + assertThat( system.isNaturalIdentifier(), is( true ) ); + + final MappingMetamodel mappingMetamodel = factoryScope.getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(); + final EntityPersister accountMapping = mappingMetamodel.findEntityDescriptor( Account.class ); + assertThat( accountMapping.hasNaturalIdentifier(), is( true ) ); + final NaturalIdMapping naturalIdMapping = accountMapping.getNaturalIdMapping(); + assertThat( naturalIdMapping, notNullValue() ); + + final List attributes = naturalIdMapping.getNaturalIdAttributes(); + assertThat( attributes.size(), is( 2 ) ); + + // alphabetical matching overall processing + + final SingularAttributeMapping first = attributes.get( 0 ); + assertThat( first, notNullValue() ); + assertThat( first.getAttributeName(), is( "system" ) ); + + final SingularAttributeMapping second = attributes.get( 1 ); + assertThat( second, notNullValue() ); + assertThat( second.getAttributeName(), is( "username" ) ); + } + + @Test + public void testGetReference(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final NaturalIdLoadAccess loadAccess = session.byNaturalId( Account.class ); + loadAccess.using( "system", "matrix" ); + loadAccess.using( "username", "neo" ); + verifyEntity( loadAccess.getReference() ); + } + ); + + scope.inTransaction( + session -> { + final MappingMetamodel mappingMetamodel = session.getFactory().getRuntimeMetamodels().getMappingMetamodel(); + final EntityPersister accountMapping = mappingMetamodel.findEntityDescriptor( Account.class ); + final NaturalIdMapping naturalIdMapping = accountMapping.getNaturalIdMapping(); + + // test load by array + Object id = naturalIdMapping.getNaturalIdLoader().resolveNaturalIdToId( VALUE_ARRAY, session ); + assertThat( id, is( 1 ) ); + + // and by Map + id = naturalIdMapping.getNaturalIdLoader().resolveNaturalIdToId( VALUE_NAP, session ); + assertThat( id, is( 1 ) ); + } + ); + } + + public void verifyEntity(Account accountRef) { + assertThat( accountRef, notNullValue() ); + assertThat( accountRef.getId(), is( 1 ) ); + assertThat( accountRef.getSystem(), is( "matrix" ) ); + assertThat( accountRef.getUsername(), is( "neo" ) ); + } + + @Test + public void testLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final Account account = session.byNaturalId( Account.class ) + .using( "system", "matrix" ) + .using( "username", "neo" ) + .load(); + verifyEntity( account ); + } + ); + + scope.inTransaction( + session -> { + final MappingMetamodel mappingMetamodel = session.getFactory().getRuntimeMetamodels().getMappingMetamodel(); + final EntityPersister accountMapping = mappingMetamodel.findEntityDescriptor( Account.class ); + final NaturalIdMapping naturalIdMapping = accountMapping.getNaturalIdMapping(); + + // test load by array + naturalIdMapping.getNaturalIdLoader().load( VALUE_ARRAY, NaturalIdLoader.LoadOptions.NONE, session ); + + // and by Map + naturalIdMapping.getNaturalIdLoader().load( VALUE_NAP, NaturalIdLoader.LoadOptions.NONE, session ); + } + ); + } + + @Test + public void testOptionalLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final NaturalIdLoadAccess loadAccess = session.byNaturalId( Account.class ); + final Optional optionalAccount = loadAccess + .using( "system", "matrix" ) + .using( "username", "neo" ) + .loadOptional(); + assertThat( optionalAccount.isPresent(), is( true ) ); + verifyEntity( optionalAccount.get() ); + } + ); + } + + @Test + public void testMultiLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final NaturalIdMultiLoadAccess loadAccess = session.byMultipleNaturalId( Account.class ); + loadAccess.enableOrderedReturn( false ); + final List accounts = loadAccess.multiLoad( + NaturalIdMultiLoadAccess.compoundValue( "system", "matrix", "username", "neo" ), + NaturalIdMultiLoadAccess.compoundValue( "system", "matrix", "username", "trinity" ) + ); + assertThat( accounts.size(), is( 2 ) ); + + final List byMap = loadAccess.multiLoad( VALUE_NAP ); + assertThat( byMap.size(), is( 1 ) ); + + final List byArray = loadAccess.multiLoad( new Object[] { VALUE_ARRAY } ); + assertThat( byArray.size(), is( 1 ) ); + } + ); + } + + + @Entity( name = "Account" ) + @Table( name = "acct" ) + public static class Account { + @Id + private Integer id; + @NaturalId + private String username; + @NaturalId + private String system; + private String emailAddress; + + public Account() { + } + + public Account(Integer id, String username, String system) { + this.id = id; + this.username = username; + this.system = system; + } + + public Account(Integer id, String username, String system, String emailAddress) { + this.id = id; + this.username = username; + this.system = system; + this.emailAddress = emailAddress; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getSystem() { + return system; + } + + public void setSystem(String system) { + this.system = system; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/mapping/naturalid/SimpleNaturalIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/mapping/naturalid/SimpleNaturalIdTests.java new file mode 100644 index 0000000000..32d2a912bc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/mapping/naturalid/SimpleNaturalIdTests.java @@ -0,0 +1,138 @@ +/* + * 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.metamodel.mapping.naturalid; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import javax.money.Monetary; + +import org.hibernate.SimpleNaturalIdLoadAccess; +import org.hibernate.NaturalIdMultiLoadAccess; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.MappingMetamodel; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.retail.Product; +import org.hibernate.testing.orm.domain.retail.Vendor; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests for simple (single attribute) natural-ids + */ +@DomainModel( standardModels = StandardDomainModel.RETAIL ) +@SessionFactory +public class SimpleNaturalIdTests { + private static final UUID uuid = UUID.randomUUID(); + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final Vendor vendor = new Vendor( 1, "Acme Brick", "Acme Global" ); + session.persist( vendor ); + + final Product product = new Product( + 1, + uuid, + vendor, + Monetary.getDefaultAmountFactory().setNumber( 1L ).setCurrency( Monetary.getCurrency( Locale.US ) ).create() + ); + session.persist( product ); + } + ); + } + + @AfterEach + public void releaseTestData(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete Product" ).executeUpdate(); + session.createQuery( "delete Vendor" ).executeUpdate(); + } + ); + } + + @Test + public void testProcessing(DomainModelScope domainModelScope, SessionFactoryScope factoryScope) { + final PersistentClass productBootMapping = domainModelScope.getDomainModel().getEntityBinding( Product.class.getName() ); + assertThat( productBootMapping.hasNaturalId(), is( true ) ); + final Property sku = productBootMapping.getProperty( "sku" ); + assertThat( sku.isNaturalIdentifier(), is( true ) ); + + final MappingMetamodel mappingMetamodel = factoryScope.getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(); + final EntityPersister productMapping = mappingMetamodel.findEntityDescriptor( Product.class ); + assertThat( productMapping.hasNaturalIdentifier(), is( true ) ); + final NaturalIdMapping naturalIdMapping = productMapping.getNaturalIdMapping(); + assertThat( naturalIdMapping, notNullValue() ); + } + + @Test + public void testGetReference(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final SimpleNaturalIdLoadAccess loadAccess = session.bySimpleNaturalId( Product.class ); + verifyEntity( loadAccess.getReference( uuid ) ); + } + ); + } + + public void verifyEntity(Product productRef) { + assertThat( productRef, notNullValue() ); + assertThat( productRef.getId(), is( 1 ) ); + assertThat( productRef.getSku(), is( uuid ) ); + } + + @Test + public void testLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final SimpleNaturalIdLoadAccess loadAccess = session.bySimpleNaturalId( Product.class ); + verifyEntity( loadAccess.load( uuid ) ); + } + ); + } + + @Test + public void testOptionalLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final SimpleNaturalIdLoadAccess loadAccess = session.bySimpleNaturalId( Product.class ); + final Optional optionalProduct = loadAccess.loadOptional( uuid ); + assertThat( optionalProduct.isPresent(), is( true ) ); + verifyEntity( optionalProduct.get() ); + } + ); + } + + @Test + public void testMultiLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final NaturalIdMultiLoadAccess loadAccess = session.byMultipleNaturalId( Product.class ); + loadAccess.enableOrderedReturn( false ); + final List products = loadAccess.multiLoad( uuid ); + assertThat( products.size(), is( 1 ) ); + verifyEntity( products.get( 0 ) ); + } + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/cfg/persister/GoofyPersisterClassProvider.java b/hibernate-core/src/test/java/org/hibernate/test/cfg/persister/GoofyPersisterClassProvider.java index d004677588..73d7d91d78 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/cfg/persister/GoofyPersisterClassProvider.java +++ b/hibernate-core/src/test/java/org/hibernate/test/cfg/persister/GoofyPersisterClassProvider.java @@ -49,12 +49,11 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.metamodel.mapping.EntityVersionMapping; import org.hibernate.metamodel.mapping.NaturalIdMapping; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.MultiLoadOptions; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.persister.spi.PersisterClassResolver; import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.persister.walking.spi.AttributeDefinition; @@ -290,7 +289,7 @@ public class GoofyPersisterClassProvider implements PersisterClassResolver { @Override public List multiLoad( - Object[] ids, SharedSessionContractImplementor session, MultiLoadOptions loadOptions) { + Object[] ids, SharedSessionContractImplementor session, MultiIdLoadOptions loadOptions) { return Collections.emptyList(); } diff --git a/hibernate-core/src/test/java/org/hibernate/test/legacy/CustomPersister.java b/hibernate-core/src/test/java/org/hibernate/test/legacy/CustomPersister.java index 9963eb2178..217974ae83 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/legacy/CustomPersister.java +++ b/hibernate-core/src/test/java/org/hibernate/test/legacy/CustomPersister.java @@ -53,11 +53,10 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.metamodel.mapping.EntityVersionMapping; import org.hibernate.metamodel.mapping.NaturalIdMapping; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.MultiLoadOptions; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.persister.walking.spi.AttributeDefinition; import org.hibernate.persister.walking.spi.EntityIdentifierDefinition; @@ -329,7 +328,7 @@ public class CustomPersister implements EntityPersister { } @Override - public List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiLoadOptions loadOptions) { + public List multiLoad(Object[] ids, SharedSessionContractImplementor session, MultiIdLoadOptions loadOptions) { return Collections.emptyList(); } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/MonetaryAmountConverter.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/MonetaryAmountConverter.java index e310f1444b..ee605d16eb 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/MonetaryAmountConverter.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/MonetaryAmountConverter.java @@ -6,6 +6,8 @@ */ package org.hibernate.testing.orm.domain; +import java.util.Locale; +import javax.money.CurrencyUnit; import javax.money.Monetary; import javax.money.MonetaryAmount; import javax.persistence.AttributeConverter; @@ -27,6 +29,6 @@ public class MonetaryAmountConverter implements AttributeConverter