From 8140c655c47e3358b291e3208ae415e09e45004f Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 19 Nov 2025 16:30:44 -0700 Subject: [PATCH 1/5] HHH-16383 - NaturalIdClass --- .../org/hibernate/NaturalIdLoadAccess.java | 4 +- .../hibernate/NaturalIdMultiLoadAccess.java | 3 + .../src/main/java/org/hibernate/Session.java | 54 ++++++ .../hibernate/SimpleNaturalIdLoadAccess.java | 3 + .../org/hibernate/annotations/NaturalId.java | 156 +++++++++--------- .../hibernate/annotations/NaturalIdClass.java | 48 ++++++ .../boot/model/internal/EntityBinder.java | 8 + .../boot/models/HibernateAnnotations.java | 4 + .../internal/NaturalIdClassAnnotation.java | 53 ++++++ .../engine/spi/SessionDelegatorBaseImpl.java | 20 +++ .../engine/spi/SessionLazyDelegator.java | 20 +++ .../NaturalIdMultiLoadAccessStandard.java | 16 ++ .../org/hibernate/internal/SessionImpl.java | 86 ++++++++++ .../internal/BaseNaturalIdLoadAccessImpl.java | 9 + .../SimpleNaturalIdLoadAccessImpl.java | 15 +- .../java/org/hibernate/mapping/RootClass.java | 9 + .../metamodel/mapping/NaturalIdMapping.java | 5 + .../internal/AbstractNaturalIdMapping.java | 4 +- .../internal/CompoundNaturalIdMapping.java | 18 +- .../internal/SimpleNaturalIdMapping.java | 7 + .../entity/AbstractEntityPersister.java | 21 ++- .../idclass/SimpleNaturalIdClassTests.java | 103 ++++++++++++ 22 files changed, 582 insertions(+), 84 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java create mode 100644 hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NaturalIdClassAnnotation.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java index d60e404cfc9b..099036bc55f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdLoadAccess.java @@ -5,7 +5,6 @@ package org.hibernate; import jakarta.persistence.EntityGraph; - import jakarta.persistence.PessimisticLockScope; import jakarta.persistence.Timeout; import jakarta.persistence.metamodel.SingularAttribute; @@ -35,7 +34,10 @@ * @see Session#byNaturalId(Class) * @see org.hibernate.annotations.NaturalId * @see SimpleNaturalIdLoadAccess + * + * @deprecated (since 7.3) Use {@linkplain Session#findByNaturalId} instead. */ +@Deprecated public interface NaturalIdLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java index 29c21475af44..d38813665990 100644 --- a/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdMultiLoadAccess.java @@ -36,7 +36,10 @@ * * @see Session#byMultipleNaturalId(Class) * @see org.hibernate.annotations.NaturalId + * + * @deprecated (since 7.3) Use {@linkplain Session#findMultipleByNaturalId} instead. */ +@Deprecated public interface NaturalIdMultiLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index cd4109bc9a24..83b7a09c51b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -558,6 +558,42 @@ public interface Session extends SharedSessionContract, EntityManager { */ Object find(String entityName, Object primaryKey, FindOption... options); + /// Find an entity by [natural-id][org.hibernate.annotations.NaturalId]. + /// + /// @param entityType The type of entity to load. + /// @param naturalId The natural-id value. + /// @param options The options to apply to the find operation. + /// + /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. + T findByNaturalId(Class entityType, Object naturalId, FindOption... options); + + /// Find an entity by [natural-id][org.hibernate.annotations.NaturalId]. + /// + /// @param entityName The name of the entity type to load. + /// @param naturalId The natural-id value. + /// @param options The options to apply to the find operation. + /// + /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. + Object findByNaturalId(String entityName, Object naturalId, FindOption... options); + + /// Find multiple entities by [natural-id][org.hibernate.annotations.NaturalId]. + /// + /// @param entityType The type of entity to load. + /// @param naturalIds The natural-id values. + /// @param options The options to apply to the find operation. May contain [FindMultipleOption] values. + /// + /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. + List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options); + + /// Find multiple entities by [natural-id][org.hibernate.annotations.NaturalId]. + /// + /// @param entityName The name of the entity type to load. + /// @param naturalIds The natural-id values. + /// @param options The options to apply to the find operation. May contain [FindMultipleOption] values. + /// + /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. + List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options); + /** * Return the persistent instances of the given entity class with the given identifiers * as a list. The position of an instance in the returned list matches the position of its @@ -1203,7 +1239,10 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given class does not resolve as a mapped entity, * or if the entity does not declare a natural id + * + * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. */ + @Deprecated NaturalIdLoadAccess byNaturalId(Class entityClass); /** @@ -1218,7 +1257,10 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given name does not resolve to a mapped entity, * or if the entity does not declare a natural id + * + * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. */ + @Deprecated NaturalIdLoadAccess byNaturalId(String entityName); /** @@ -1233,7 +1275,10 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given class does not resolve as a mapped entity, * or if the entity does not declare a natural id + * + * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. */ + @Deprecated SimpleNaturalIdLoadAccess bySimpleNaturalId(Class entityClass); /** @@ -1248,7 +1293,10 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given name does not resolve to a mapped entity, * or if the entity does not declare a natural id + * + * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. */ + @Deprecated SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName); /** @@ -1262,7 +1310,10 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given class does not resolve as a mapped entity, * or if the entity does not declare a natural id + * + * @deprecated (since 7.3) : Use {@linkplain #findMultipleByNaturalId} instead. */ + @Deprecated NaturalIdMultiLoadAccess byMultipleNaturalId(Class entityClass); /** @@ -1276,7 +1327,10 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given name does not resolve to a mapped entity, * or if the entity does not declare a natural id + * + * @deprecated (since 7.3) : Use {@linkplain #findMultipleByNaturalId} instead. */ + @Deprecated NaturalIdMultiLoadAccess byMultipleNaturalId(String entityName); /** diff --git a/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java index 13130fd7c18e..bdcb8a61de8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/SimpleNaturalIdLoadAccess.java @@ -29,7 +29,10 @@ * @see Session#bySimpleNaturalId(Class) * @see org.hibernate.annotations.NaturalId * @see NaturalIdLoadAccess + * + * @deprecated (since 7.3) Use {@linkplain Session#findByNaturalId} instead. */ +@Deprecated public interface SimpleNaturalIdLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java index 4eca9fc13bab..6cfb11a3ada9 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalId.java @@ -4,6 +4,7 @@ */ package org.hibernate.annotations; + import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -11,80 +12,87 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; -/** - * Specifies that a field or property of an entity class is part of - * the natural id of the entity. This annotation is very useful when - * the primary key of an entity class is a surrogate key, that is, - * a {@linkplain jakarta.persistence.GeneratedValue system-generated} - * synthetic identifier, with no domain-model semantics. There should - * always be some other field or combination of fields which uniquely - * identifies an instance of the entity from the point of view of the - * user of the system. This is the natural id of the entity. - *

- * A natural id may be a single field or property of the entity: - *

- * @Entity
- * @Cache @NaturalIdCache
- * class Person {
- *
- *     //synthetic id
- *     @GeneratedValue @Id
- *     Long id;
- *
- *     @NotNull
- *     String name;
- *
- *     //simple natural id
- *     @NotNull @NaturalId
- *     String ssn;
- *
- *     ...
- * }
- * 
- *

- * or it may be a composite value: - *

- * @Entity
- * @Cache @NaturalIdCache
- * class Vehicle {
- *
- *     //synthetic id
- *     @GeneratedValue @Id
- *     Long id;
- *
- *     //composite natural id
- *
- *     @Enumerated
- *     @NotNull @NaturalId
- *     Region region;
- *
- *     @NotNull @NaturalId
- *     String registration;
- *
- *     ...
- * }
- * 
- *

- * Unlike the {@linkplain jakarta.persistence.Id primary identifier} - * of an entity, a natural id may be {@linkplain #mutable}. - *

- * On the other hand, a field or property which forms part of a natural - * id may never be null, and so it's a good idea to use {@code @NaturalId} - * in conjunction with the Bean Validation {@code @NotNull} annotation - * or {@link jakarta.persistence.Basic#optional @Basic(optional=false)}. - *

- * The {@link org.hibernate.Session} interface offers several methods - * that allow an entity instance to be retrieved by its - * {@linkplain org.hibernate.Session#bySimpleNaturalId(Class) simple} - * or {@linkplain org.hibernate.Session#byNaturalId(Class) composite} - * natural id value. If the entity is also marked for {@linkplain - * NaturalIdCache natural id caching}, then these methods may be able - * to avoid a database round trip. - * - * @author Nicolás Lichtmaier - * - * @see NaturalIdCache - */ +/// Specifies that a field or property of an entity class is part of +/// the natural id of the entity. This annotation is very useful when +/// the primary key of an entity class is a surrogate key, that is, +/// a {@linkplain jakarta.persistence.GeneratedValue system-generated} +/// synthetic identifier, with no domain-model semantics. There should +/// always be some other field or combination of fields which uniquely +/// identifies an instance of the entity from the point of view of the +/// user of the system. This is the _natural id_ of the entity. +/// +/// A natural id may be a single (basic or embedded) attribute of the entity: +/// ````java +/// @Entity +/// class Person { +/// +/// //synthetic id +/// @GeneratedValue @Id +/// Long id; +/// +/// @NotNull +/// String name; +/// +/// //simple natural id +/// @NotNull @NaturalId +/// String ssn; +/// +/// ... +/// } +/// ``` +/// +/// or it may be a non-aggregated composite value: +/// ```java +/// @Entity +/// class Vehicle { +/// +/// //synthetic id +/// @GeneratedValue @Id +/// Long id; +/// +/// //composite natural id +/// +/// @Enumerated +/// @NotNull +/// @NaturalId +/// Region region; +/// +/// @NotNull +/// @NaturalId +/// String registration; +/// +/// ... +/// } +/// ``` +/// +/// Unlike the {@linkplain jakarta.persistence.Id primary identifier} +/// of an entity, a natural id may be {@linkplain #mutable}. +/// +/// On the other hand, a field or property which forms part of a natural +/// id may never be null, and so it's a good idea to use `@NaturalId` +/// in conjunction with the Bean Validation `@NotNull` annotation +/// or [@Basic(optional=false)][jakarta.persistence.Basic#optional()]. +/// +/// The [org.hibernate.Session] interface offers several methods +/// that allow retrieval of one or more entity references by natural-id +/// allow an entity instance to be retrieved by its natural-id: +/// * [org.hibernate.Session#findByNaturalId] allows loading a single +/// entity instance by natural-id. +/// * [org.hibernate.Session#findMultipleByNaturalId] allows loading multiple +/// entity instances by natural-id. +/// +/// If the entity is also marked for [natural id caching][NaturalIdCache], +/// then these methods may be able to avoid a database round trip. +/// +/// @see org.hibernate.Session#findByNaturalId +/// @see org.hibernate.Session#findMultipleByNaturalId +/// @see NaturalIdClass +/// @see NaturalIdCache +/// +/// @apiNote For non-aggregated composite natural-id cases, it is recommended to +/// leverage [@NaturalIdClass][NaturalIdClass] for loading. +/// +/// @author Nicolás Lichtmaier @Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface NaturalId { diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java new file mode 100644 index 000000000000..05d5dfcca7d4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +/// Models a non-aggregated composite natural-id for the purpose of loading. +/// The non-aggregated form uses multiple [@NaturalId][NaturalId] as opposed +/// to the aggregated form which uses a single [@NaturalId][NaturalId] combined +/// with [@Embedded][jakarta.persistence.Embedded]. +/// Functions in a similar fashion as [@IdClass][jakarta.persistence.IdClass] for +/// non-aggregated composite identifiers. +/// +/// ```java +/// @Entity +/// @NaturalIdClass(OrderNaturalId.class) +/// class Order { +/// @Id +/// Integer id; +/// @NaturalId @ManyToOne +/// Customer customer; +/// @NaturalId +/// Integer orderNumber; +/// ... +/// } +/// +/// class OrderNaturalId { +/// Integer customer; +/// Integer orderNumber; +/// ... +/// } +/// ``` +/// +/// @see NaturalId +/// @see jakarta.persistence.IdClass +/// +/// @author Steve Ebersole +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface NaturalIdClass { + /// The class to use for loading the associated entity by natural-id. + Class value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index e553f89e1c2b..20e30c187f06 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -159,6 +159,7 @@ public class EntityBinder { private String cacheRegion; private boolean cacheLazyProperty; private String naturalIdCacheRegion; + private Class naturalIdClass; private CacheLayout queryCacheLayout; private ModelsContext modelsContext() { @@ -1264,6 +1265,7 @@ private void bindEntity() { bindSqlRestriction(); bindCache(); bindNaturalIdCache(); + bindNaturalIdClass(); bindFiltersInHierarchy(); persistentClass.setAbstract( annotatedClass.isAbstract() ); @@ -1338,6 +1340,7 @@ private void bindRootEntity() { rootClass.setLazyPropertiesCacheable( cacheLazyProperty ); } rootClass.setNaturalIdCacheRegionName( naturalIdCacheRegion ); + rootClass.setNaturalIdClass( naturalIdClass ); } private void bindCustomSql() { @@ -1613,6 +1616,11 @@ private SQLRestriction extractSQLRestriction(ClassDetails classDetails) { return null; } + private void bindNaturalIdClass() { + final var ann = annotatedClass.getAnnotationUsage( NaturalIdClass.class, modelsContext() ); + naturalIdClass = ann != null ? ann.value() : null; + } + private void bindNaturalIdCache() { final var naturalIdCache = annotatedClass.getAnnotationUsage( NaturalIdCache.class, modelsContext() ); if ( naturalIdCache != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java b/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java index 7c343f3bac4b..08f5522c47c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java @@ -475,6 +475,10 @@ public interface HibernateAnnotations { NaturalIdCache.class, NaturalIdCacheAnnotation.class ); + OrmAnnotationDescriptor NATURAL_ID_CLASS = new OrmAnnotationDescriptor<>( + NaturalIdClass.class, + NaturalIdClassAnnotation.class + ); OrmAnnotationDescriptor NOT_FOUND = new OrmAnnotationDescriptor<>( NotFound.class, NotFoundAnnotation.class diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NaturalIdClassAnnotation.java b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NaturalIdClassAnnotation.java new file mode 100644 index 000000000000..654b5e292dc6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NaturalIdClassAnnotation.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.models.annotations.internal; + +import org.hibernate.annotations.NaturalIdClass; +import org.hibernate.models.spi.ModelsContext; + +import java.lang.annotation.Annotation; +import java.util.Map; + +@SuppressWarnings({ "ClassExplicitlyAnnotation", "unused" }) +@jakarta.annotation.Generated("org.hibernate.orm.build.annotations.ClassGeneratorProcessor") +public class NaturalIdClassAnnotation implements NaturalIdClass { + private Class value; + + /** + * Used in creating dynamic annotation instances (e.g. from XML) + */ + public NaturalIdClassAnnotation(ModelsContext modelContext) { + this.value = void.class; + } + + /** + * Used in creating annotation instances from JDK variant + */ + public NaturalIdClassAnnotation(NaturalIdClass annotation, ModelsContext modelContext) { + this.value = annotation.value(); + } + + /** + * Used in creating annotation instances from Jandex variant + */ + public NaturalIdClassAnnotation(Map attributeValues, ModelsContext modelContext) { + this.value = (Class) attributeValues.get( "value" ); + } + + @Override + public Class annotationType() { + return NaturalIdClass.class; + } + + @Override + public Class value() { + return value; + } + + public void value(Class value) { + this.value = value; + } + +} 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 492ecd4462c8..774d7567b83d 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 @@ -906,6 +906,26 @@ public Object find(String entityName, Object primaryKey, FindOption... options) return delegate.find( entityName, primaryKey, options ); } + @Override + public T findByNaturalId(Class entityType, Object naturalId, FindOption... options) { + return delegate.findByNaturalId( entityType, naturalId, options ); + } + + @Override + public Object findByNaturalId(String entityName, Object naturalId, FindOption... options) { + return delegate.findByNaturalId( entityName, naturalId, options ); + } + + @Override + public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { + return delegate.findMultipleByNaturalId( entityType, naturalIds, options ); + } + + @Override + public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { + return delegate.findMultipleByNaturalId( entityName, naturalIds, options ); + } + @Override public T getReference(Class entityClass, Object id) { return delegate.getReference( entityClass, id ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java index 19fbf55a7a03..a1f4c5beeb0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java @@ -820,6 +820,26 @@ public Object find(String entityName, Object primaryKey, FindOption... options) return this.lazySession.get().find( entityName, primaryKey, options ); } + @Override + public T findByNaturalId(Class entityType, Object naturalId, FindOption... options) { + return lazySession.get().findByNaturalId( entityType, naturalId, options ); + } + + @Override + public Object findByNaturalId(String entityName, Object naturalId, FindOption... options) { + return lazySession.get().findByNaturalId( entityName, naturalId, options ); + } + + @Override + public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { + return lazySession.get().findMultipleByNaturalId( entityType, naturalIds, options ); + } + + @Override + public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { + return lazySession.get().findMultipleByNaturalId( entityName, naturalIds, options ); + } + @Override public void lock(Object entity, LockModeType lockMode) { this.lazySession.get().lock( entity, lockMode ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index bb84eb30e4e6..7d65812fd4a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -13,6 +13,7 @@ import org.hibernate.CacheMode; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; import org.hibernate.NaturalIdMultiLoadAccess; import org.hibernate.OrderingMode; import org.hibernate.RemovalsMode; @@ -56,6 +57,13 @@ public NaturalIdMultiLoadAccess with(LockMode lockMode, PessimisticLockScope return this; } + public void with(Locking.Scope scope) { + if ( lockOptions == null ) { + lockOptions = new LockOptions(); + } + lockOptions.setScope( scope ); + } + @Override public NaturalIdMultiLoadAccess with(Timeout timeout) { if ( lockOptions == null ) { @@ -96,12 +104,20 @@ public NaturalIdMultiLoadAccess enableReturnOfDeletedEntities(boolean enabled return this; } + public void with(RemovalsMode removalsMode) { + this.removalsMode = removalsMode; + } + @Override public NaturalIdMultiLoadAccess enableOrderedReturn(boolean enabled) { this.orderingMode = enabled ? OrderingMode.ORDERED : OrderingMode.UNORDERED; return this; } + public void with(OrderingMode orderingMode) { + this.orderingMode = orderingMode; + } + @Override @SuppressWarnings( "unchecked" ) public List multiLoad(Object... ids) { 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 a37ec581e42c..7de12c4410b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -2400,6 +2400,92 @@ public Object find(String entityName, Object primaryKey, FindOption... options) return loadAccess.load( primaryKey ); } + @Override + public T findByNaturalId(Class entityType, Object naturalId, FindOption... options) { + final SimpleNaturalIdLoadAccessImpl access = (SimpleNaturalIdLoadAccessImpl) bySimpleNaturalId( entityType ); + setOptions( options, access ); + return access.load( naturalId ); + } + + private void setOptions(FindOption[] options, SimpleNaturalIdLoadAccessImpl access) { + for ( FindOption option : options ) { + if ( option instanceof LockMode lockMode ) { + access.with( lockMode ); + } + else if ( option instanceof LockModeType lockModeType ) { + access.with( LockMode.fromJpaLockMode( lockModeType ) ); + } + else if ( option instanceof Locking.Scope scope ) { + access.with( scope ); + } + else if ( option instanceof PessimisticLockScope scope ) { + access.with( Locking.Scope.fromJpaScope( scope ) ); + } + else if ( option instanceof Timeout timeout ) { + access.with( timeout ); + } + else { + throw new IllegalArgumentException( "Illegal option: " + option ); + } + } + } + + @Override + public Object findByNaturalId(String entityName, Object naturalId, FindOption... options) { + final SimpleNaturalIdLoadAccessImpl access = (SimpleNaturalIdLoadAccessImpl) bySimpleNaturalId( entityName ); + setOptions( options, access ); + return access.load( naturalId ); + } + + @Override + public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { + final NaturalIdMultiLoadAccessStandard access = (NaturalIdMultiLoadAccessStandard) byMultipleNaturalId( entityType ); + setOptions( options, access ); + return access.multiLoad( naturalIds ); + } + + private void setOptions(FindOption[] options, NaturalIdMultiLoadAccessStandard access) { + for ( FindOption option : options ) { + if ( option instanceof LockMode lockMode ) { + access.with( lockMode ); + } + else if ( option instanceof LockModeType lockModeType ) { + access.with( LockMode.fromJpaLockMode( lockModeType ) ); + } + else if ( option instanceof Locking.Scope scope ) { + access.with( scope ); + } + else if ( option instanceof PessimisticLockScope scope ) { + access.with( Locking.Scope.fromJpaScope( scope ) ); + } + else if ( option instanceof Timeout timeout ) { + access.with( timeout ); + } + else if ( option instanceof CacheMode cacheMode ) { + access.with( cacheMode ); + } + else if ( option instanceof BatchSize batchSize ) { + access.withBatchSize( batchSize.batchSize() ); + } + else if ( option instanceof RemovalsMode removalsMode ) { + access.with( removalsMode ); + } + else if ( option instanceof OrderingMode orderingMode ) { + access.with( orderingMode ); + } + else { + throw new IllegalArgumentException( "Illegal option: " + option ); + } + } + } + + @Override + public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { + final NaturalIdMultiLoadAccessStandard access = (NaturalIdMultiLoadAccessStandard) byMultipleNaturalId( entityName ); + setOptions( options, access ); + return access.multiLoad( naturalIds ); + } + @Override public T getReference(Class entityClass, Object id) { checkOpen(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java index 14685b1c9ba7..1de0d68f623f 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/internal/BaseNaturalIdLoadAccessImpl.java @@ -15,6 +15,7 @@ import org.hibernate.IdentifierLoadAccess; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; import org.hibernate.UnknownProfileException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.Status; @@ -70,6 +71,14 @@ protected Object with(LockMode lockMode, PessimisticLockScope lockScope) { return this; } + protected Object with(Locking.Scope lockScope) { + if ( lockOptions == null ) { + lockOptions = new LockOptions(); + } + lockOptions.setScope( lockScope ); + return this; + } + protected Object with(Timeout timeout) { if ( lockOptions == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java index f041771aaa06..c083911ffed7 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/internal/SimpleNaturalIdLoadAccessImpl.java @@ -16,10 +16,12 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; import org.hibernate.SimpleNaturalIdLoadAccess; import org.hibernate.graph.GraphSemantic; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping; +import org.hibernate.persister.entity.EntityPersister; /** * Implementation of {@link SimpleNaturalIdLoadAccess}. @@ -57,6 +59,11 @@ public SimpleNaturalIdLoadAccess with(LockMode lockMode, PessimisticLockScope return (SimpleNaturalIdLoadAccess) super.with( lockMode, lockScope ); } + public SimpleNaturalIdLoadAccess with(Locking.Scope lockScope) { + super.with( lockScope ); + return this; + } + @Override public SimpleNaturalIdLoadAccess with(Timeout timeout) { //noinspection unchecked @@ -99,7 +106,8 @@ private void verifySimplicity(Object naturalIdValue) { if ( !hasSimpleNaturalId && !naturalIdValue.getClass().isArray() && !(naturalIdValue instanceof List) - && !(naturalIdValue instanceof Map) ) { + && !(naturalIdValue instanceof Map) + && ! ( isNaturalIdClass( naturalIdValue ) ) ) { throw new HibernateException( String.format( Locale.ROOT, @@ -111,6 +119,11 @@ private void verifySimplicity(Object naturalIdValue) { } } + private boolean isNaturalIdClass(Object naturalIdValue) { + final EntityPersister entityPersister = entityPersister(); + return entityPersister.getNaturalIdMapping().getNaturalIdClass().isInstance( naturalIdValue ); + } + @Override public Optional loadOptional(Object naturalIdValue) { return Optional.ofNullable( load( naturalIdValue ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java index 6503fa477807..8925218d4c31 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java @@ -34,6 +34,7 @@ public final class RootClass extends PersistentClass implements TableOwner, Soft private String cacheConcurrencyStrategy; private String cacheRegionName; private boolean lazyPropertiesCacheable = true; + private Class naturalIdClass; private String naturalIdCacheRegionName; private Value discriminator; @@ -365,6 +366,14 @@ public void setLazyPropertiesCacheable(boolean lazyPropertiesCacheable) { this.lazyPropertiesCacheable = lazyPropertiesCacheable; } + public Class getNaturalIdClass() { + return naturalIdClass; + } + + public void setNaturalIdClass(Class naturalIdClass) { + this.naturalIdClass = naturalIdClass; + } + @Override public String getNaturalIdCacheRegionName() { return naturalIdCacheRegionName; 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 8ecf1ef79562..8b46f4ad29c9 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 @@ -6,6 +6,7 @@ import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Incubating; import org.hibernate.cache.spi.access.NaturalIdDataAccess; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -42,6 +43,10 @@ public interface NaturalIdMapping extends VirtualModelPart { String PART_NAME = "{natural-id}"; + /// A class which used as a wrapper for natural-id values + /// during load similar to [jakarta.persistence.IdClass]. + @Nullable Class getNaturalIdClass(); + /** * The attribute(s) making up the natural-id. */ 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 index 52254432816e..bc048fff29c3 100644 --- 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 @@ -19,7 +19,9 @@ public abstract class AbstractNaturalIdMapping implements NaturalIdMapping { private final NavigableRole role; - public AbstractNaturalIdMapping(EntityMappingType declaringType, boolean mutable) { + public AbstractNaturalIdMapping( + EntityMappingType declaringType, + boolean mutable) { this.declaringType = declaringType; this.mutable = mutable; 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 index 395328a6595e..924defe8f713 100644 --- 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 @@ -10,6 +10,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.cache.MutableCacheKeyBuilder; @@ -49,9 +50,8 @@ */ public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implements MappingType, FetchableContainer { - // todo (6.0) : create a composite MappingType for this descriptor's Object[]? - private final List attributes; + private final Class naturalIdClass; private List jdbcMappings; /* @@ -62,9 +62,11 @@ This value is used to determine the size of the array used to create the Immutab public CompoundNaturalIdMapping( EntityMappingType declaringType, + Class naturalIdClass, List attributes, MappingModelCreationProcess creationProcess) { super( declaringType, isMutable( attributes ) ); + this.naturalIdClass = naturalIdClass; this.attributes = attributes; int maxIndex = 0; @@ -99,6 +101,12 @@ private static boolean isMutable(List attributes) { return false; } + @Override + @Nullable + public Class getNaturalIdClass() { + return naturalIdClass; + } + @Override public Object[] extractNaturalIdFromEntityState(Object[] state) { if ( state == null ) { @@ -128,7 +136,11 @@ public Object[] extractNaturalIdFromEntity(Object entity) { @Override public Object[] normalizeInput(Object incoming) { - if ( incoming instanceof Object[] array ) { + if ( getNaturalIdClass() != null + && getNaturalIdClass().isInstance( incoming ) ) { + throw new UnsupportedOperationException( "NaturalIdClass support not implemented yet" ); + } + else if ( incoming instanceof Object[] array ) { return array; } else if ( incoming instanceof Map valueMap ) { 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 index 555117bf9d97..d51ba7540d2e 100644 --- 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 @@ -10,6 +10,7 @@ import java.util.Map; import java.util.function.BiConsumer; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.dialect.Dialect; @@ -170,6 +171,12 @@ public List getNaturalIdAttributes() { return Collections.singletonList( attribute ); } + @Override + @Nullable + public Class getNaturalIdClass() { + return null; + } + @Override public MappingType getPartMappingType() { return attribute.getPartMappingType(); 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 76d9652c8e94..58d0946d338c 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 @@ -4851,8 +4851,9 @@ else if ( bootEntityDescriptor.hasNaturalId() ) { } } - protected NaturalIdMapping generateNaturalIdMapping - (MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { + protected NaturalIdMapping generateNaturalIdMapping( + MappingModelCreationProcess creationProcess, + PersistentClass bootEntityDescriptor) { //noinspection AssertWithSideEffects assert bootEntityDescriptor.hasNaturalId(); @@ -4860,9 +4861,16 @@ else if ( bootEntityDescriptor.hasNaturalId() ) { assert naturalIdAttributeIndexes.length > 0; if ( naturalIdAttributeIndexes.length == 1 ) { + if ( bootEntityDescriptor.getRootClass().getNaturalIdClass() != null ) { + throw new UnsupportedMappingException( "NaturalIdClass not supported for simple naturaal-id mappings" ); + } final String propertyName = getPropertyNames()[ naturalIdAttributeIndexes[ 0 ] ]; final var attributeMapping = (SingularAttributeMapping) findAttributeMapping( propertyName ); - return new SimpleNaturalIdMapping( attributeMapping, this, creationProcess ); + return new SimpleNaturalIdMapping( + attributeMapping, + this, + creationProcess + ); } // collect the names of the attributes making up the natural-id. @@ -4886,7 +4894,12 @@ else if ( bootEntityDescriptor.hasNaturalId() ) { throw new MappingException( "Expected multiple natural-id attributes, but found only one: " + getEntityName() ); } - return new CompoundNaturalIdMapping(this, collectedAttrMappings, creationProcess ); + return new CompoundNaturalIdMapping( + this, + bootEntityDescriptor.getRootClass().getNaturalIdClass(), + collectedAttrMappings, + creationProcess + ); } protected static SqmMultiTableMutationStrategy interpretSqmMultiTableStrategy( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java new file mode 100644 index 000000000000..c1076fd7fa09 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.naturalid.idclass; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdClass; +import org.hibernate.mapping.RootClass; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.ExpectedException; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = { + SimpleNaturalIdClassTests.Key.class, + SimpleNaturalIdClassTests.TestEntity.class, + SimpleNaturalIdClassTests.TestEntity2.class +}) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16383" ) +public class SimpleNaturalIdClassTests { + @Test + void testBootModel(DomainModelScope modelScope) { + modelScope.getDomainModel().validate(); + final RootClass entity1Binding = (RootClass) modelScope.getEntityBinding( TestEntity.class ); + assertEquals( Key.class, entity1Binding.getNaturalIdClass() ); + final RootClass entity2Binding = (RootClass) modelScope.getEntityBinding( TestEntity2.class ); + assertNull( entity2Binding.getNaturalIdClass() ); + } + + @Test + @SessionFactory + void findBySimpleSmokeTest(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.findByNaturalId( TestEntity2.class, "steve" ); + } ); + } + + @Test + @SessionFactory + void findMultipleBySimpleSmokeTest(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.findMultipleByNaturalId( TestEntity2.class, List.of( "steve", "john" ) ); + } ); + } + + @Test + @SessionFactory + @ExpectedException( UnsupportedOperationException.class ) + void findByClassSmokeTest(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.findByNaturalId( TestEntity.class, new Key("steve", "ebersole") ); + } ); + } + + @Test + @SessionFactory + @ExpectedException( UnsupportedOperationException.class ) + void findMultipleByClassSmokeTest(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.findMultipleByNaturalId( TestEntity.class, List.of( new Key("steve", "ebersole") ) ); + } ); + } + + @Entity(name="TestEntity") + @Table(name="TestEntity") + @NaturalIdClass(Key.class) + public static class TestEntity { + @Id + private Integer id; + @NaturalId + private String name1; + @NaturalId + private String name2; + } + + public record Key(String name1, String name2) { + } + + @Entity(name="TestEntity2") + @Table(name="TestEntity2") + public static class TestEntity2 { + @Id + private Integer id; + @NaturalId + private String name; + } +} From e3245cb2af4c3d0986e04d3cae18972451fdfd04 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 20 Nov 2025 09:28:43 -0700 Subject: [PATCH 2/5] HHH-16383 - NaturalIdClass # Conflicts: # hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java --- .../boot/model/internal/EntityBinder.java | 52 +++- .../NaturalIdMultiLoadAccessStandard.java | 5 + .../AbstractMultiNaturalIdLoader.java | 3 +- .../java/org/hibernate/mapping/RootClass.java | 7 +- .../internal/CompoundNaturalIdMapping.java | 231 ++++++++++++++++-- .../idclass/SimpleNaturalIdClassTests.java | 5 +- 6 files changed, 264 insertions(+), 39 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index 20e30c187f06..607a8c53567e 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -29,7 +29,40 @@ import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; import org.hibernate.MappingException; -import org.hibernate.annotations.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.annotations.Check; +import org.hibernate.annotations.Checks; +import org.hibernate.annotations.ConcreteProxy; +import org.hibernate.annotations.DiscriminatorFormula; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.Filters; +import org.hibernate.annotations.HQLSelect; +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Mutability; +import org.hibernate.annotations.NaturalIdCache; +import org.hibernate.annotations.NaturalIdClass; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OptimisticLockType; +import org.hibernate.annotations.OptimisticLocking; +import org.hibernate.annotations.QueryCacheLayout; +import org.hibernate.annotations.RowId; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLDeleteAll; +import org.hibernate.annotations.SQLInsert; +import org.hibernate.annotations.SQLRestriction; +import org.hibernate.annotations.SQLSelect; +import org.hibernate.annotations.SQLUpdate; +import org.hibernate.annotations.SecondaryRow; +import org.hibernate.annotations.SecondaryRows; +import org.hibernate.annotations.SoftDelete; +import org.hibernate.annotations.Subselect; +import org.hibernate.annotations.Synchronize; +import org.hibernate.annotations.TypeBinderType; +import org.hibernate.annotations.View; import org.hibernate.binder.TypeBinder; import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.model.internal.InheritanceState.ElementsToProcess; @@ -86,6 +119,7 @@ import static jakarta.persistence.InheritanceType.SINGLE_TABLE; import static java.util.Collections.addAll; +import static org.hibernate.boot.BootLogging.BOOT_LOGGER; import static org.hibernate.boot.model.internal.AnnotatedClassType.MAPPED_SUPERCLASS; import static org.hibernate.boot.model.internal.AnnotatedDiscriminatorColumn.DEFAULT_DISCRIMINATOR_COLUMN_NAME; import static org.hibernate.boot.model.internal.AnnotatedDiscriminatorColumn.buildDiscriminatorColumn; @@ -112,7 +146,6 @@ import static org.hibernate.boot.spi.AccessType.getAccessStrategy; import static org.hibernate.engine.OptimisticLockStyle.fromLockType; import static org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle.fromResultCheckStyle; -import static org.hibernate.boot.BootLogging.BOOT_LOGGER; import static org.hibernate.internal.util.ReflectHelper.getDefaultSupplier; import static org.hibernate.internal.util.StringHelper.isBlank; import static org.hibernate.internal.util.StringHelper.isNotBlank; @@ -159,7 +192,7 @@ public class EntityBinder { private String cacheRegion; private boolean cacheLazyProperty; private String naturalIdCacheRegion; - private Class naturalIdClass; + private ClassDetails naturalIdClass; private CacheLayout queryCacheLayout; private ModelsContext modelsContext() { @@ -1340,7 +1373,7 @@ private void bindRootEntity() { rootClass.setLazyPropertiesCacheable( cacheLazyProperty ); } rootClass.setNaturalIdCacheRegionName( naturalIdCacheRegion ); - rootClass.setNaturalIdClass( naturalIdClass ); + rootClass.setNaturalIdClass( naturalIdClass ); } private void bindCustomSql() { @@ -1618,7 +1651,16 @@ private SQLRestriction extractSQLRestriction(ClassDetails classDetails) { private void bindNaturalIdClass() { final var ann = annotatedClass.getAnnotationUsage( NaturalIdClass.class, modelsContext() ); - naturalIdClass = ann != null ? ann.value() : null; + if ( ann != null ) { + if ( ann.value() == void.class ) { + throw new IllegalStateException( "NaturalIdClass#value must not be void.class" ); + } + naturalIdClass = context + .getBootstrapContext() + .getModelsContext() + .getClassDetailsRegistry() + .resolveClassDetails( ann.value().getName() ); + } } private void bindNaturalIdCache() { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index 7d65812fd4a0..f9de72ab4dac 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -151,6 +151,11 @@ public List multiLoad(Object... ids) { } try { + // normalize the incoming natural-id values + for ( int i = 0; i < ids.length; i++ ) { + ids[i] = entityDescriptor.getNaturalIdMapping().normalizeInput( ids[ i ] ); + } + return (List) entityDescriptor.getMultiNaturalIdLoader() .multiLoad( ids, this, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java index 8ac139d2d38d..513907e72b2c 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java @@ -144,9 +144,8 @@ private Object[] checkPersistenceContextForCachedResults( Consumer results ) { final List unresolvedIds = arrayList( naturalIds.length ); final var context = session.getPersistenceContextInternal(); - final var naturalIdMapping = getEntityDescriptor().getNaturalIdMapping(); for ( K naturalId : naturalIds ) { - final Object entity = entityForNaturalId( context, naturalIdMapping.normalizeInput( naturalId ) ); + final Object entity = entityForNaturalId( context, naturalId ); if ( entity != null ) { // Entity is already in the persistence context final var entry = context.getEntry( entity ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java index 8925218d4c31..a4ec66d3a364 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java @@ -12,6 +12,7 @@ import org.hibernate.annotations.SoftDeleteType; import org.hibernate.boot.Metadata; import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.models.spi.ClassDetails; import static org.hibernate.internal.CoreMessageLogger.CORE_LOGGER; import static org.hibernate.internal.util.ReflectHelper.overridesEquals; @@ -34,7 +35,7 @@ public final class RootClass extends PersistentClass implements TableOwner, Soft private String cacheConcurrencyStrategy; private String cacheRegionName; private boolean lazyPropertiesCacheable = true; - private Class naturalIdClass; + private ClassDetails naturalIdClass; private String naturalIdCacheRegionName; private Value discriminator; @@ -366,11 +367,11 @@ public void setLazyPropertiesCacheable(boolean lazyPropertiesCacheable) { this.lazyPropertiesCacheable = lazyPropertiesCacheable; } - public Class getNaturalIdClass() { + public ClassDetails getNaturalIdClass() { return naturalIdClass; } - public void setNaturalIdClass(Class naturalIdClass) { + public void setNaturalIdClass(ClassDetails naturalIdClass) { this.naturalIdClass = naturalIdClass; } 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 index 924defe8f713..a25edefc9c45 100644 --- 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 @@ -4,15 +4,10 @@ */ 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.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; +import org.hibernate.MappingException; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.IndexedConsumer; @@ -21,6 +16,7 @@ import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; import org.hibernate.loader.ast.spi.NaturalIdLoader; import org.hibernate.metamodel.UnsupportedMappingException; +import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; @@ -28,6 +24,11 @@ import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.models.spi.ClassDetails; +import org.hibernate.models.spi.ModelsContext; +import org.hibernate.property.access.spi.Getter; +import org.hibernate.property.access.spi.GetterFieldImpl; +import org.hibernate.property.access.spi.GetterMethodImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -45,13 +46,25 @@ import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.type.descriptor.java.JavaType; +import java.beans.Introspector; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + /** * Multi-attribute NaturalIdMapping implementation */ public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implements MappingType, FetchableContainer { private final List attributes; - private final Class naturalIdClass; + private final KeyClassNormalizer keyClassNormalizer; private List jdbcMappings; /* @@ -62,11 +75,11 @@ This value is used to determine the size of the array used to create the Immutab public CompoundNaturalIdMapping( EntityMappingType declaringType, - Class naturalIdClass, + ClassDetails naturalIdClass, List attributes, MappingModelCreationProcess creationProcess) { super( declaringType, isMutable( attributes ) ); - this.naturalIdClass = naturalIdClass; + this.keyClassNormalizer = KeyClassNormalizer.create( naturalIdClass, attributes, creationProcess ); this.attributes = attributes; int maxIndex = 0; @@ -104,7 +117,7 @@ private static boolean isMutable(List attributes) { @Override @Nullable public Class getNaturalIdClass() { - return naturalIdClass; + return keyClassNormalizer == null ? null : keyClassNormalizer.idClassType; } @Override @@ -127,7 +140,7 @@ else if ( state.length == attributes.size() ) { @Override public Object[] extractNaturalIdFromEntity(Object entity) { - final var values = new Object[ attributes.size() ]; + final var values = new Object[attributes.size()]; for ( int i = 0; i < attributes.size(); i++ ) { values[i] = attributes.get( i ).getPropertyAccess().getGetter().get( entity ); } @@ -136,18 +149,18 @@ public Object[] extractNaturalIdFromEntity(Object entity) { @Override public Object[] normalizeInput(Object incoming) { - if ( getNaturalIdClass() != null - && getNaturalIdClass().isInstance( incoming ) ) { - throw new UnsupportedOperationException( "NaturalIdClass support not implemented yet" ); + if ( keyClassNormalizer != null + && keyClassNormalizer.idClassType.isInstance( incoming ) ) { + return keyClassNormalizer.normalize( incoming ); } else if ( incoming instanceof Object[] array ) { return array; } - else if ( incoming instanceof Map valueMap ) { + else if ( incoming instanceof Map valueMap ) { final var attributes = getNaturalIdAttributes(); - final var values = new Object[ attributes.size() ]; + final var values = new Object[attributes.size()]; for ( int i = 0; i < attributes.size(); i++ ) { - values[ i ] = valueMap.get( attributes.get( i ).getAttributeName() ); + values[i] = valueMap.get( attributes.get( i ).getAttributeName() ); } return values; } @@ -355,7 +368,7 @@ else if ( domainValue instanceof Object[] values ) { } } else { - throw new AssertionFailure("Unexpected domain value type"); + throw new AssertionFailure( "Unexpected domain value type" ); } return span; } @@ -406,7 +419,7 @@ else if ( value instanceof Object[] incoming ) { return outgoing; } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } } @@ -424,7 +437,7 @@ else if ( value instanceof Object[] values ) { } } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } } @@ -465,7 +478,7 @@ else if ( value instanceof Object[] incoming ) { } } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } return span; } @@ -493,7 +506,7 @@ else if ( value instanceof Object[] incoming ) { } } else { - throw new AssertionFailure("Unexpected value"); + throw new AssertionFailure( "Unexpected value" ); } return span; } @@ -521,7 +534,7 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { @Override public void forEachSubPart(IndexedConsumer consumer, EntityMappingType treatTarget) { for ( int i = 0; i < attributes.size(); i++ ) { - consumer.accept( i, attributes.get(i) ); + consumer.accept( i, attributes.get( i ) ); } } @@ -642,9 +655,9 @@ private AssemblerImpl(ImmutableFetchList fetches, JavaType jtd, Assemb @Override public Object[] assemble(RowProcessingState rowProcessingState) { - final var result = new Object[ subAssemblers.length ]; + final var result = new Object[subAssemblers.length]; for ( int i = 0; i < subAssemblers.length; i++ ) { - result[ i ] = subAssemblers[i].assemble( rowProcessingState ); + result[i] = subAssemblers[i].assemble( rowProcessingState ); } return result; } @@ -674,4 +687,172 @@ public JavaType getAssembledJavaType() { } } + /// Responsible for decomposing a value of the NaturalIdClass into the internal array format + public record KeyClassNormalizer( + Class idClassType, + List> attributeMappers) { + public Object[] normalize(T idClassValue) { + final Object[] result = new Object[attributeMappers.size()]; + for ( int i = 0; i < attributeMappers.size(); i++ ) { + var value = attributeMappers.get( i ).extractFrom( idClassValue ); + result[i] = value; + } + return result; + } + + public static KeyClassNormalizer create( + ClassDetails naturalIdClassDetails, + List keyAttributes, + MappingModelCreationProcess creationProcess) { + if ( naturalIdClassDetails == null ) { + return null; + } + + final ModelsContext modelsContext = creationProcess + .getCreationContext() + .getBootstrapContext() + .getModelsContext(); + + var naturalIdClass = naturalIdClassDetails.toJavaClass( modelsContext.getClassLoading(), modelsContext ); + var naturalIdClassComponents = extractComponents( naturalIdClass ); + var naturalIdClassGetterAccess = createNaturalIdClassGetterAccess( naturalIdClass ); + + final List> attributeMappers = new ArrayList<>(); + keyAttributes.forEach( (keyAttribute) -> { + // find the matching MemberDetails on the `naturalIdClass`... + final Getter extractor = resolveMatchingExtractor( + naturalIdClass, + keyAttribute, + naturalIdClassGetterAccess, + naturalIdClassComponents, + modelsContext + ); + final AttributeMapper attrMapper; + if ( keyAttribute instanceof ToOneAttributeMapping ) { + attrMapper = new ToOneAttributeMapperImpl<>( keyAttribute, extractor ); + } + else { + attrMapper = new BasicAttributeMapperImpl<>( keyAttribute, extractor ); + } + attributeMappers.add( attrMapper ); + } ); + + //noinspection unchecked,rawtypes + return new KeyClassNormalizer( naturalIdClass, attributeMappers ); + } + + private static Function createNaturalIdClassGetterAccess(Class naturalIdClass) { + return new Function<>() { + private Map getterMethods; + @Override + public Method apply(String name) { + if ( getterMethods == null ) { + getterMethods = extractGetterMethods( naturalIdClass ); + } + return getterMethods.get( name ); + } + }; + } + + private static Getter resolveMatchingExtractor( + Class naturalIdClass, + AttributeMapping keyAttribute, + Function getterMethodAccess, + Map naturalIdClassComponents, + ModelsContext modelsContext) { + // first, if the `naturalIdClass` is a record, look for a component + if ( naturalIdClass.isRecord() ) { + var component = naturalIdClassComponents.get( keyAttribute.getAttributeName() ); + if ( component != null ) { + return new GetterMethodImpl( + naturalIdClass, + keyAttribute.getAttributeName(), + component.getAccessor() + ); + } + } + + // next look for a getter method + var getterMethod = getterMethodAccess.apply( keyAttribute.getAttributeName() ); + if ( getterMethod != null ) { + return new GetterMethodImpl( + naturalIdClass, + keyAttribute.getAttributeName(), + getterMethod + ); + } + + // lastly, look for a field + try { + var field = naturalIdClass.getDeclaredField( keyAttribute.getAttributeName() ); + return new GetterFieldImpl( naturalIdClass, keyAttribute.getAttributeName(), field ); + } + catch (NoSuchFieldException ignore) { + } + + throw new MappingException( "Unable to find NaturalIdClass accessor for natural-id attribute: " + keyAttribute.getAttributeName() ); + } + + private static Map extractGetterMethods(Class naturalIdClass) { + final Map result = new HashMap<>(); + + for ( Method declaredMethod : naturalIdClass.getDeclaredMethods() ) { + if ( declaredMethod.getParameterCount() == 0 + && declaredMethod.getReturnType() != void.class + && !Modifier.isStatic( declaredMethod.getModifiers() ) ) { + var methodName = declaredMethod.getName(); + if ( methodName.startsWith( "is" ) ) { + result.put( + Introspector.decapitalize( methodName.substring( 2 ) ), + declaredMethod + ); + } + else if ( methodName.startsWith( "get" ) ) { + result.put( + Introspector.decapitalize( methodName.substring( 3 ) ), + declaredMethod + ); + } + } + } + + return result; + } + + private static Map extractComponents(Class naturalIdClass) { + if ( !naturalIdClass.isRecord() ) { + return Map.of(); + } + + final RecordComponent[] recordComponents = naturalIdClass.getRecordComponents(); + final Map result = new HashMap<>(); + for ( RecordComponent recordComponent : recordComponents ) { + result.put( recordComponent.getName(), recordComponent ); + } + return result; + } + } + + public interface AttributeMapper { + V extractFrom(T keyValue); + } + + /// AttributeMapper for both basic and embedded values + public record BasicAttributeMapperImpl(AttributeMapping entityAttribute, Getter keyClassExtractor) + implements AttributeMapper { + @Override + public Object extractFrom(T keyValue) { + return keyClassExtractor.get( keyValue ); + } + } + + /// AttributeMapper for to-one values + public record ToOneAttributeMapperImpl(AttributeMapping entityAttribute, Getter keyClassExtractor) + implements AttributeMapper { + @Override + public Object extractFrom(T keyValue) { + // todo (natural-id-class) : handle "key -> to-one" resolutions + return keyClassExtractor.get( keyValue ); + } + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java index c1076fd7fa09..8c5dd4a8b27f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java @@ -12,7 +12,6 @@ import org.hibernate.mapping.RootClass; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.DomainModelScope; -import org.hibernate.testing.orm.junit.ExpectedException; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -38,7 +37,7 @@ public class SimpleNaturalIdClassTests { void testBootModel(DomainModelScope modelScope) { modelScope.getDomainModel().validate(); final RootClass entity1Binding = (RootClass) modelScope.getEntityBinding( TestEntity.class ); - assertEquals( Key.class, entity1Binding.getNaturalIdClass() ); + assertEquals( Key.class, entity1Binding.getNaturalIdClass().toJavaClass() ); final RootClass entity2Binding = (RootClass) modelScope.getEntityBinding( TestEntity2.class ); assertNull( entity2Binding.getNaturalIdClass() ); } @@ -61,7 +60,6 @@ void findMultipleBySimpleSmokeTest(SessionFactoryScope factoryScope) { @Test @SessionFactory - @ExpectedException( UnsupportedOperationException.class ) void findByClassSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { session.findByNaturalId( TestEntity.class, new Key("steve", "ebersole") ); @@ -70,7 +68,6 @@ void findByClassSmokeTest(SessionFactoryScope factoryScope) { @Test @SessionFactory - @ExpectedException( UnsupportedOperationException.class ) void findMultipleByClassSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { session.findMultipleByNaturalId( TestEntity.class, List.of( new Key("steve", "ebersole") ) ); From 70e476b787bc1d8952a2319e586904b9d7989238 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 20 Nov 2025 12:13:19 -0700 Subject: [PATCH 3/5] HHH-16383 - NaturalIdClass --- .../src/main/java/org/hibernate/FindBy.java | 33 +++++ .../src/main/java/org/hibernate/Session.java | 4 +- .../engine/spi/SessionDelegatorBaseImpl.java | 4 +- .../engine/spi/SessionLazyDelegator.java | 4 +- .../org/hibernate/internal/SessionImpl.java | 68 ++++++++-- .../internal/CompoundNaturalIdMapping.java | 10 ++ .../idclass/SimpleNaturalIdClassTests.java | 71 ++++++++-- .../naturalid/idclass/ToOneTests.java | 122 ++++++++++++++++++ 8 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/FindBy.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/FindBy.java b/hibernate-core/src/main/java/org/hibernate/FindBy.java new file mode 100644 index 000000000000..13b92a57d9d7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/FindBy.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +import jakarta.persistence.FindOption; + +/// FindOption allowing to load based on either id (default) +/// or natural-id. +/// +/// @see jakarta.persistence.EntityManager#find +/// @see Session#findMultiple +/// +/// @author Steve Ebersole +/// @author Gavin King +public enum FindBy implements FindOption { + /// Indicates to find by the entity's identifier. The default. + /// + /// @see jakarta.persistence.Id + /// @see jakarta.persistence.EmbeddedId + /// @see jakarta.persistence.IdClass + ID, + + /// Indicates to find based on the entity's natural-id, if one. + /// + /// @see org.hibernate.annotations.NaturalId + /// @see org.hibernate.annotations.NaturalIdClass + /// + /// @implSpec Will trigger an [IllegalArgumentException] if the entity does + /// not define a natural-id. + NATURAL_ID +} diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 83b7a09c51b7..05e76ae5b606 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -583,7 +583,7 @@ public interface Session extends SharedSessionContract, EntityManager { /// @param options The options to apply to the find operation. May contain [FindMultipleOption] values. /// /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. - List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options); + List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options); /// Find multiple entities by [natural-id][org.hibernate.annotations.NaturalId]. /// @@ -592,7 +592,7 @@ public interface Session extends SharedSessionContract, EntityManager { /// @param options The options to apply to the find operation. May contain [FindMultipleOption] values. /// /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. - List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options); + List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options); /** * Return the persistent instances of the given entity class with the given identifiers 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 774d7567b83d..530fe2441c2d 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 @@ -917,12 +917,12 @@ public Object findByNaturalId(String entityName, Object naturalId, FindOption... } @Override - public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { + public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { return delegate.findMultipleByNaturalId( entityType, naturalIds, options ); } @Override - public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { + public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { return delegate.findMultipleByNaturalId( entityName, naturalIds, options ); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java index a1f4c5beeb0f..85857554d989 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java @@ -831,12 +831,12 @@ public Object findByNaturalId(String entityName, Object naturalId, FindOption... } @Override - public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { + public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { return lazySession.get().findMultipleByNaturalId( entityType, naturalIds, options ); } @Override - public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { + public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { return lazySession.get().findMultipleByNaturalId( entityName, naturalIds, options ); } 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 7de12c4410b2..b0a09b5bd558 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -928,12 +928,17 @@ public void load(Object object, Object id) { fireLoad( new LoadEvent( id, object, this, getReadOnlyFromLoadQueryInfluencers() ), LoadEventListener.RELOAD ); } - private void setMultiIdentifierLoadAccessOptions(FindOption[] options, MultiIdentifierLoadAccess loadAccess) { + private boolean setMultiIdentifierLoadAccessOptions(FindOption[] options, MultiIdentifierLoadAccess loadAccess) { CacheStoreMode storeMode = getCacheStoreMode(); CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); LockOptions lockOptions = copySessionLockOptions(); int batchSize = -1; for ( FindOption option : options ) { + if ( option instanceof FindBy findBy ) { + if ( findBy == FindBy.NATURAL_ID ) { + return true; + } + } if ( option instanceof CacheStoreMode cacheStoreMode ) { storeMode = cacheStoreMode; } @@ -981,12 +986,17 @@ else if ( option instanceof RemovalsMode ) { loadAccess.with( lockOptions ) .with( interpretCacheMode( storeMode, retrieveMode ) ) .withBatchSize( batchSize ); + + return false; } @Override public List findMultiple(Class entityType, List ids, FindOption... options) { final var loadAccess = byMultipleIds( entityType ); - setMultiIdentifierLoadAccessOptions( options, loadAccess ); + final boolean isFindByNaturalId = setMultiIdentifierLoadAccessOptions( options, loadAccess ); + if ( isFindByNaturalId ) { + return findMultipleByNaturalId( entityType, ids, options ); + } return loadAccess.multiLoad( ids ); } @@ -1000,7 +1010,10 @@ public List findMultiple(EntityGraph entityGraph, List ids, FindOpt case POJO -> byMultipleIds( type.getJavaType() ); }; loadAccess.withLoadGraph( rootGraph ); - setMultiIdentifierLoadAccessOptions( options, loadAccess ); + final boolean isFindByNaturalId = setMultiIdentifierLoadAccessOptions( options, loadAccess ); + if ( isFindByNaturalId ) { + throw new UnsupportedOperationException( "Find by natural-id with entity-graph is not supported" ); + } return loadAccess.multiLoad( ids ); } @@ -2251,12 +2264,18 @@ protected static void logIgnoringEntityNotFound(Class entityClass, Object } } - private void setLoadAccessOptions(FindOption[] options, IdentifierLoadAccessImpl loadAccess) { + /// @return `true` if [FindBy#NATURAL_ID] was found + private boolean setLoadAccessOptions(FindOption[] options, IdentifierLoadAccessImpl loadAccess) { CacheStoreMode storeMode = getCacheStoreMode(); CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); LockOptions lockOptions = copySessionLockOptions(); for ( FindOption option : options ) { - if ( option instanceof CacheStoreMode cacheStoreMode ) { + if ( option instanceof FindBy findBy ) { + if ( findBy == FindBy.NATURAL_ID ) { + return true; + } + } + else if ( option instanceof CacheStoreMode cacheStoreMode ) { storeMode = cacheStoreMode; } else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { @@ -2306,13 +2325,18 @@ else if ( option instanceof FindMultipleOption findMultipleOption ) { } } loadAccess.with( lockOptions ).with( interpretCacheMode( storeMode, retrieveMode ) ); + + return false; } @Override - public T find(Class entityClass, Object primaryKey, FindOption... options) { + public T find(Class entityClass, Object key, FindOption... options) { final IdentifierLoadAccessImpl loadAccess = byId( entityClass ); - setLoadAccessOptions( options, loadAccess ); - return loadAccess.load( primaryKey ); + final boolean isFindByNaturalId = setLoadAccessOptions( options, loadAccess ); + if ( isFindByNaturalId ) { + return findByNaturalId( entityClass, key, options ); + } + return loadAccess.load( key ); } @Override @@ -2324,7 +2348,10 @@ public T find(EntityGraph entityGraph, Object primaryKey, FindOption... o case MAP -> byId( type.getTypeName() ); case POJO -> byId( type.getJavaType() ); }; - setLoadAccessOptions( options, loadAccess ); + final boolean isFindByNaturalId = setLoadAccessOptions( options, loadAccess ); + if ( isFindByNaturalId ) { + throw new UnsupportedOperationException( "Find by natural-id with entity-graph is not supported" ); + } return loadAccess.withLoadGraph( graph ).load( primaryKey ); } @@ -2396,7 +2423,10 @@ public Object find(String entityName, Object primaryKey) { @Override public Object find(String entityName, Object primaryKey, FindOption... options) { final IdentifierLoadAccessImpl loadAccess = byId( entityName ); - setLoadAccessOptions( options, loadAccess ); + final boolean isFindByNaturalId = setLoadAccessOptions( options, loadAccess ); + if ( isFindByNaturalId ) { + return findByNaturalId( entityName, primaryKey, options ); + } return loadAccess.load( primaryKey ); } @@ -2409,7 +2439,12 @@ public T findByNaturalId(Class entityType, Object naturalId, FindOption.. private void setOptions(FindOption[] options, SimpleNaturalIdLoadAccessImpl access) { for ( FindOption option : options ) { - if ( option instanceof LockMode lockMode ) { + if ( option instanceof FindBy findBy ) { + if ( findBy == FindBy.ID ) { + throw new IllegalArgumentException( "Cannot use FindBy#ID with findByNaturalId" ); + } + } + else if ( option instanceof LockMode lockMode ) { access.with( lockMode ); } else if ( option instanceof LockModeType lockModeType ) { @@ -2438,7 +2473,7 @@ public Object findByNaturalId(String entityName, Object naturalId, FindOption... } @Override - public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { + public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { final NaturalIdMultiLoadAccessStandard access = (NaturalIdMultiLoadAccessStandard) byMultipleNaturalId( entityType ); setOptions( options, access ); return access.multiLoad( naturalIds ); @@ -2446,7 +2481,12 @@ public List findMultipleByNaturalId(Class entityType, List nat private void setOptions(FindOption[] options, NaturalIdMultiLoadAccessStandard access) { for ( FindOption option : options ) { - if ( option instanceof LockMode lockMode ) { + if ( option instanceof FindBy findBy ) { + if ( findBy == FindBy.ID ) { + throw new IllegalArgumentException( "Cannot use FindBy#ID with findMultipleByNaturalId" ); + } + } + else if ( option instanceof LockMode lockMode ) { access.with( lockMode ); } else if ( option instanceof LockModeType lockModeType ) { @@ -2480,7 +2520,7 @@ else if ( option instanceof OrderingMode orderingMode ) { } @Override - public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { + public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { final NaturalIdMultiLoadAccessStandard access = (NaturalIdMultiLoadAccessStandard) byMultipleNaturalId( entityName ); setOptions( options, access ); return access.multiLoad( naturalIds ); 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 index a25edefc9c45..34ddaa7437ed 100644 --- 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 @@ -727,6 +727,10 @@ public static KeyClassNormalizer create( naturalIdClassComponents, modelsContext ); + // todo (natural-id-class) : atm there is functionally no difference + // between BasicAttributeMapperImpl and ToOneAttributeMapperImpl. + // ideally we'd eventually support usage of the associated key entity's + // id and then there would. see the note in ToOneAttributeMapperImpl#extractFrom final AttributeMapper attrMapper; if ( keyAttribute instanceof ToOneAttributeMapping ) { attrMapper = new ToOneAttributeMapperImpl<>( keyAttribute, extractor ); @@ -852,6 +856,12 @@ public record ToOneAttributeMapperImpl(AttributeMapping entityAttribute, Gett @Override public Object extractFrom(T keyValue) { // todo (natural-id-class) : handle "key -> to-one" resolutions + // this requires some contract changes though to pass Session + // to be able to resolve key -> entity for the to-one. + // + + /// the other difficulty is handling "derived id" structures + // + // see `NaturalIdMapping#normalizeInput` return keyClassExtractor.get( keyValue ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java index 8c5dd4a8b27f..d4d85891c72a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java @@ -7,6 +7,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.hibernate.FindBy; import org.hibernate.annotations.NaturalId; import org.hibernate.annotations.NaturalIdClass; import org.hibernate.mapping.RootClass; @@ -15,10 +16,13 @@ import org.hibernate.testing.orm.junit.Jira; 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 java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -32,7 +36,21 @@ SimpleNaturalIdClassTests.TestEntity2.class }) @Jira( "https://hibernate.atlassian.net/browse/HHH-16383" ) +@SessionFactory public class SimpleNaturalIdClassTests { + @BeforeEach + void setUp(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.persist( new TestEntity( 1, "steve", "ebersole" ) ); + session.persist( new TestEntity2( 1, "steve" ) ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + @Test void testBootModel(DomainModelScope modelScope) { modelScope.getDomainModel().validate(); @@ -43,34 +61,54 @@ void testBootModel(DomainModelScope modelScope) { } @Test - @SessionFactory void findBySimpleSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - session.findByNaturalId( TestEntity2.class, "steve" ); + var result = session.findByNaturalId( TestEntity2.class, "steve" ); + assertEquals( 1, result.id ); } ); } @Test - @SessionFactory void findMultipleBySimpleSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - session.findMultipleByNaturalId( TestEntity2.class, List.of( "steve", "john" ) ); + var results = session.findMultipleByNaturalId( TestEntity2.class, List.of( "steve", "john" ) ); + assertThat( results ).hasSize( 2 ); + assertEquals( 1, results.get( 0 ).id ); + assertNull( results.get( 1 ) ); } ); } @Test - @SessionFactory void findByClassSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - session.findByNaturalId( TestEntity.class, new Key("steve", "ebersole") ); + var result = session.findByNaturalId( TestEntity.class, new Key("steve", "ebersole") ); + assertEquals( 1, result.id ); + } ); + } + + @Test + void findByClassFindBySmokeTest(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var result = session.find( TestEntity.class, new Key("steve", "ebersole"), FindBy.NATURAL_ID ); + assertEquals( 1, result.id ); } ); } @Test - @SessionFactory void findMultipleByClassSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - session.findMultipleByNaturalId( TestEntity.class, List.of( new Key("steve", "ebersole") ) ); + var results = session.findMultipleByNaturalId( TestEntity.class, List.of( new Key("steve", "ebersole") ) ); + assertThat( results ).hasSize( 1 ); + assertEquals( 1, results.get( 0 ).id ); + } ); + } + + @Test + void findMultipleByClassFindBySmokeTest(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var results = session.findMultiple( TestEntity.class, List.of( new Key("steve", "ebersole") ), FindBy.NATURAL_ID ); + assertThat( results ).hasSize( 1 ); + assertEquals( 1, results.get( 0 ).id ); } ); } @@ -84,6 +122,15 @@ public static class TestEntity { private String name1; @NaturalId private String name2; + + public TestEntity() { + } + + public TestEntity(Integer id, String name1, String name2) { + this.id = id; + this.name1 = name1; + this.name2 = name2; + } } public record Key(String name1, String name2) { @@ -96,5 +143,13 @@ public static class TestEntity2 { private Integer id; @NaturalId private String name; + + public TestEntity2() { + } + + public TestEntity2(Integer id, String name) { + this.id = id; + this.name = name; + } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java new file mode 100644 index 000000000000..0307279eaf80 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.naturalid.idclass; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.FindBy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdClass; +import org.hibernate.property.access.spi.PropertyAccessException; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ExpectedException; +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 java.time.Instant; +import java.util.Map; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = { + ToOneTests.OrderKey.class, + ToOneTests.Customer.class, + ToOneTests.Order.class +}) +@SessionFactory +public class ToOneTests { + @BeforeEach + void setUp(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Customer billys = new Customer( 1, "bILLY bOB'S Steak House and Grill and Bar and Sushi" ); + session.persist( billys ); + + final Order billys1001 = new Order( 1, billys, 1001, Instant.now() ); + session.persist( billys1001 ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + + @Test + @ExpectedException(PropertyAccessException.class) + void testWouldBeNiceBaseline(SessionFactoryScope factoryScope) { + // This would be functionally equivalent to defining + // OrderKey with `int customer` and simply using the Customer id. + // However, this is not supported atm. + // See comments in `CompoundNaturalIdMapping.ToOneAttributeMapperImpl#extractFrom`. + factoryScope.inTransaction( (session) -> { + session.findByNaturalId( + Order.class, + Map.of( "customer", 1, "invoiceNumber", 1001 ) + ); + } ); + } + + @Test + void testCurrentReality(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + // the current reality is that we need a reference to the associated entity + // rather than just its id + var customer = session.getReference( Customer.class, 1 ); + session.find( Order.class, new OrderKey(customer, 1001), FindBy.NATURAL_ID ); + } ); + } + + @Entity(name="Customer") + @Table(name="customers") + public static class Customer { + @Id + private Integer id; + private String name; + + public Customer() { + } + + public Customer(Integer id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name="Order") + @Table(name="orders") + @NaturalIdClass(OrderKey.class) + public static class Order { + @Id + private Integer id; + @NaturalId + @ManyToOne + @JoinColumn(name = "customer_fk") + private Customer customer; + @NaturalId + int invoiceNumber; + private Instant timestamp; + + public Order() { + } + + public Order(Integer id, Customer customer, int invoiceNumber, Instant timestamp) { + this.id = id; + this.customer = customer; + this.invoiceNumber = invoiceNumber; + this.timestamp = timestamp; + } + } + + public record OrderKey(Customer customer, int invoiceNumber) { + } +} From 159be18c29e565bae8076dffe08bb810b361687e Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Fri, 21 Nov 2025 10:23:39 -0700 Subject: [PATCH 4/5] HHH-16383 - NaturalIdClass --- .../hibernate/NaturalIdSynchronization.java | 19 + .../src/main/java/org/hibernate/Session.java | 2478 ++++++++--------- .../src/main/java/org/hibernate/Timeouts.java | 13 + .../engine/spi/SessionDelegatorBaseImpl.java | 20 - .../engine/spi/SessionLazyDelegator.java | 20 - .../NaturalIdMultiLoadAccessStandard.java | 4 +- .../org/hibernate/internal/SessionImpl.java | 211 +- .../internal/find/FindByKeyOperation.java | 335 +++ .../find/FindMultipleByKeyOperation.java | 339 +++ .../org/hibernate/internal/find/Helper.java | 31 + .../ast/internal/AbstractNaturalIdLoader.java | 38 +- .../loader/ast/spi/NaturalIdLoader.java | 52 +- .../idclass/SimpleNaturalIdClassTests.java | 15 +- .../naturalid/idclass/ToOneTests.java | 5 +- 14 files changed, 2016 insertions(+), 1564 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java create mode 100644 hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java create mode 100644 hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java diff --git a/hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java b/hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java new file mode 100644 index 000000000000..60a775a19bf1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +import jakarta.persistence.FindOption; + +/// Indicates whether to perform synchronization (a sort of flush) +/// before a [find by natural-id][FindBy#NATURAL_ID]. +/// +/// @author Steve Ebersole +public enum NaturalIdSynchronization implements FindOption { + /// Perform the synchronization. + ENABLED, + + /// Do not perform the synchronization. + DISABLED; +} diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 05e76ae5b606..aed9e9a00cea 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -4,1568 +4,1348 @@ */ package org.hibernate; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - -import jakarta.persistence.FindOption; -import jakarta.persistence.LockOption; -import jakarta.persistence.RefreshOption; -import jakarta.persistence.metamodel.EntityType; -import org.hibernate.graph.RootGraph; -import org.hibernate.jdbc.Work; -import org.hibernate.query.Query; -import org.hibernate.stat.SessionStatistics; - import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; +import jakarta.persistence.FindOption; import jakarta.persistence.FlushModeType; import jakarta.persistence.LockModeType; +import jakarta.persistence.LockOption; import jakarta.persistence.TypedQueryReference; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.metamodel.EntityType; +import org.hibernate.graph.RootGraph; +import org.hibernate.query.Query; +import org.hibernate.stat.SessionStatistics; -/** - * The main runtime interface between a Java application and Hibernate. Represents the - * notion of a persistence context, a set of managed entity instances associated - * with a logical transaction. - *

- * The lifecycle of a {@code Session} is bounded by the beginning and end of the logical - * transaction. But a long logical transaction might span several database transactions. - *

- * The primary purpose of the {@code Session} is to offer create, read, and delete - * operations for instances of mapped entity classes. An instance may be in one of three - * states with respect to a given open session: - *

    - *
  • transient: never persistent, and not associated with the {@code Session}, - *
  • persistent: currently associated with the {@code Session}, or - *
  • detached: previously persistent, but not currently associated with the - * {@code Session}. - *
- *

- * Each persistent instance has a persistent identity determined by its type - * and identifier value. There may be at most one persistent instance with a given - * persistent identity associated with a given session. A persistent identity is - * assigned when an {@linkplain #persist(Object) instance is made persistent}. - *

- * An instance of an entity class may be associated with at most one open session. - * Distinct sessions represent state with the same persistent identity using distinct - * persistent instances of the mapped entity class. - *

- * Any instance returned by {@link #get(Class, Object)}, {@link #find(Class, Object)}, - * or by a query is persistent. A persistent instance might hold references to other - * entity instances, and sometimes these references are proxied by an - * intermediate object. When an associated entity has not yet been fetched from the - * database, references to the unfetched entity are represented by uninitialized - * proxies. The state of an unfetched entity is automatically fetched from the - * database when a method of its proxy is invoked, if and only if the proxy is - * associated with an open session. Otherwise, {@link #getReference(Object)} may be - * used to trade a proxy belonging to a closed session for a new proxy associated - * with the current session. - *

- * A transient instance may be made persistent by calling {@link #persist(Object)}. - * A persistent instance may be made detached by calling {@link #detach(Object)}. - * A persistent instance may be marked for removal, and eventually made transient, - * by calling {@link #remove(Object)}. - *

- * Persistent instances are held in a managed state by the persistence context. Any - * change to the state of a persistent instance is automatically detected and eventually - * flushed to the database. This process of automatic change detection is called - * dirty checking and can be expensive in some circumstances. Dirty checking - * may be disabled by marking an entity as read-only using - * {@link #setReadOnly(Object, boolean)} or simply by {@linkplain #detach(Object) evicting} - * it from the persistence context. A session may be set to load entities as read-only - * {@linkplain #setDefaultReadOnly(boolean) by default}, or this may be controlled at the - * {@linkplain Query#setReadOnly(boolean) query level}. - *

- * The state of a transient or detached instance may be made persistent by copying it to - * a persistent instance using {@link #merge(Object)}. All older operations which moved a - * detached instance to the persistent state are now deprecated, and clients should now - * migrate to the use of {@code merge()}. - *

- * The persistent state of a managed entity may be refreshed from the database, discarding - * all modifications to the object held in memory, by calling {@link #refresh(Object)}. - *

- * From {@linkplain FlushMode time to time}, a {@linkplain #flush() flush operation} is - * triggered, and the session synchronizes state held in memory with persistent state - * held in the database by executing SQL {@code insert}, {@code update}, and {@code delete} - * statements. Note that SQL statements are often not executed synchronously by the methods - * of the {@code Session} interface. If synchronous execution of SQL is desired, the - * {@link StatelessSession} allows this. - *

- * Each managed instance has an associated {@link LockMode}. By default, the session - * obtains only {@link LockMode#READ} on an entity instance it reads from the database - * and {@link LockMode#WRITE} on an entity instance it writes to the database. This - * behavior is appropriate for programs which use optimistic locking. - *

    - *
  • A different lock level may be obtained by explicitly specifying the mode using - * {@link #find(Class,Object,LockModeType)}, {@link #find(Class,Object,FindOption...)}, - * {@link #refresh(Object,LockModeType)}, {@link #refresh(Object,RefreshOption...)}, - * or {@link org.hibernate.query.SelectionQuery#setLockMode(LockModeType)}. - *
  • The lock level of a managed instance already held by the session may be upgraded - * to a more restrictive lock level by calling {@link #lock(Object, LockMode)} or - * {@link #lock(Object, LockModeType)}. - *
- *

- * A persistence context holds hard references to all its entities and prevents them - * from being garbage collected. Therefore, a {@code Session} is a short-lived object, - * and must be discarded as soon as a logical transaction ends. In extreme cases, - * {@link #clear()} and {@link #detach(Object)} may be used to control memory usage. - * However, for processes which read many entities, a {@link StatelessSession} should - * be used. - *

- * A session might be associated with a container-managed JTA transaction, or it might be - * in control of its own resource-local database transaction. In the case of a - * resource-local transaction, the client must demarcate the beginning and end of the - * transaction using a {@link Transaction}. A typical resource-local transaction should - * use the following idiom: - *

- * Session session = factory.openSession();
- * Transaction tx = null;
- * try {
- *     tx = session.beginTransaction();
- *     //do some work
- *     ...
- *     tx.commit();
- * }
- * catch (Exception e) {
- *     if (tx!=null) tx.rollback();
- *     throw e;
- * }
- * finally {
- *     session.close();
- * }
- * 
- *

- * It's crucially important to appreciate the following restrictions and why they exist: - *

    - *
  • If the {@code Session} throws an exception, the current transaction must be rolled - * back and the session must be discarded. The internal state of the {@code Session} - * cannot be expected to be consistent with the database after the exception occurs. - *
  • At the end of a logical transaction, the session must be explicitly {@linkplain - * #close() destroyed}, so that all JDBC resources may be released. - *
  • If a transaction is rolled back, the state of the persistence context and of its - * associated entities must be assumed inconsistent with the database, and the - * session must be discarded. - *
  • A {@code Session} is never thread-safe. It contains various different sorts of - * fragile mutable state. Each thread or transaction must obtain its own dedicated - * instance from the {@link SessionFactory}. - *
- *

- * An easy way to be sure that session and transaction management is being done correctly - * is to {@linkplain SessionFactory#inTransaction(Consumer) let the factory do it}: - *

- * sessionFactory.inTransaction(session -> {
- *     //do the work
- *     ...
- * });
- * 
- *

- * A session may be used to {@linkplain #doWork(Work) execute JDBC work} using its JDBC - * connection and transaction: - *

- * session.doWork(connection -> {
- *     try ( PreparedStatement ps = connection.prepareStatement( " ... " ) ) {
- *         ps.execute();
- *     }
- * });
- * 
- *

- * A {@code Session} instance is serializable if its entities are serializable. - *

- * Every {@code Session} is a JPA {@link EntityManager}. Furthermore, when Hibernate is - * acting as the JPA persistence provider, the method {@link EntityManager#unwrap(Class)} - * may be used to obtain the underlying {@code Session}. - *

- * Hibernate, unlike JPA, allows a persistence unit where an entity class is mapped multiple - * times, with different entity names, usually to different tables. In this case, the session - * needs a way to identify the entity name of a given instance of the entity class. Therefore, - * some operations of this interface, including operations inherited from {@code EntityManager}, - * are overloaded with a form that accepts an explicit entity name along with the instance. An - * alternative solution to this problem is to provide an {@link EntityNameResolver}. - * - * @see SessionFactory - * - * @author Gavin King - * @author Steve Ebersole - */ +import java.util.Collection; +import java.util.List; + +/// The main runtime interface between a Java application and Hibernate. Represents the +/// notion of a _persistence context_, a set of managed entity instances associated +/// with a logical transaction. +/// +/// The lifecycle of a `Session` is bounded by the beginning and end of the logical +/// transaction. But a long logical transaction might span several database transactions. +/// +/// The primary purpose of the `Session` is to offer create, read, and delete +/// operations for instances of mapped entity classes. An instance may be in one of three +/// states with respect to a given open session: +/// +/// - _transient:_ never persistent, and not associated with the `Session`, +/// - _persistent:_ currently associated with the `Session`, or +/// - _detached:_ previously persistent, but not currently associated with the +/// `Session`. +/// +/// Each persistent instance has a _persistent identity_ determined by its type +/// and identifier value. There may be at most one persistent instance with a given +/// persistent identity associated with a given session. A persistent identity is +/// assigned when an {@linkplain #persist(Object) instance is made persistent}. +/// +/// An instance of an entity class may be associated with at most one open session. +/// Distinct sessions represent state with the same persistent identity using distinct +/// persistent instances of the mapped entity class. +/// +/// Any instance returned by [#get(Class,Object)], [#find(Class,Object)], +/// or by a query is persistent. A persistent instance might hold references to other +/// entity instances, and sometimes these references are _proxied_ by an +/// intermediate object. When an associated entity has not yet been fetched from the +/// database, references to the unfetched entity are represented by uninitialized +/// proxies. The state of an unfetched entity is automatically fetched from the +/// database when a method of its proxy is invoked, if and only if the proxy is +/// associated with an open session. Otherwise, [#getReference(Object)] may be +/// used to trade a proxy belonging to a closed session for a new proxy associated +/// with the current session. +/// +/// A transient instance may be made persistent by calling [#persist(Object)]. +/// A persistent instance may be made detached by calling [#detach(Object)]. +/// A persistent instance may be marked for removal, and eventually made transient, +/// by calling [#remove(Object)]. +/// +/// Persistent instances are held in a managed state by the persistence context. Any +/// change to the state of a persistent instance is automatically detected and eventually +/// flushed to the database. This process of automatic change detection is called +/// _dirty checking_ and can be expensive in some circumstances. Dirty checking +/// may be disabled by marking an entity as read-only using +/// [#setReadOnly(Object,boolean)] or simply by [evicting][#detach(Object)] +/// it from the persistence context. A session may be set to load entities as read-only +/// [by default][#setDefaultReadOnly(boolean)], or this may be controlled at the +/// [query level][Query#setReadOnly(boolean)]. +/// +/// The state of a transient or detached instance may be made persistent by copying it to +/// a persistent instance using [#merge(Object)]. All older operations which moved a +/// detached instance to the persistent state are now deprecated, and clients should now +/// migrate to the use of `merge()`. +/// +/// The persistent state of a managed entity may be refreshed from the database, discarding +/// all modifications to the object held in memory, by calling [#refresh(Object)]. +/// +/// From {@linkplain FlushMode time to time}, a {@linkplain #flush() flush operation} is +/// triggered, and the session synchronizes state held in memory with persistent state +/// held in the database by executing SQL `insert`, `update`, and `delete` +/// statements. Note that SQL statements are often not executed synchronously by the methods +/// of the `Session` interface. If synchronous execution of SQL is desired, the +/// [StatelessSession] allows this. +/// +/// Each managed instance has an associated [LockMode]. By default, the session +/// obtains only [LockMode#READ] on an entity instance it reads from the database +/// and [LockMode#WRITE] on an entity instance it writes to the database. This +/// behavior is appropriate for programs which use optimistic locking. +/// +/// - A different lock level may be obtained by explicitly specifying the mode using +/// [#find(Class,Object,LockModeType)], [#find(Class,Object,FindOption...)], +/// [#refresh(Object,LockModeType)], [#refresh(Object,RefreshOption...)], +/// or [org.hibernate.query.SelectionQuery#setLockMode(LockModeType)]. +/// - The lock level of a managed instance already held by the session may be upgraded +/// to a more restrictive lock level by calling [#lock(Object,LockMode)] or +/// [#lock(Object,LockModeType)]. +/// +/// A persistence context holds hard references to all its entities and prevents them +/// from being garbage collected. Therefore, a `Session` is a short-lived object, +/// and must be discarded as soon as a logical transaction ends. In extreme cases, +/// [#clear()] and [#detach(Object)] may be used to control memory usage. +/// However, for processes which read many entities, a [StatelessSession] should +/// be used. +/// +/// A session might be associated with a container-managed JTA transaction, or it might be +/// in control of its own _resource-local_ database transaction. In the case of a +/// resource-local transaction, the client must demarcate the beginning and end of the +/// transaction using a [Transaction]. A typical resource-local transaction should +/// use the following idiom: +/// +/// ```java +/// try (Session session = factory.openSession()) { +/// Transaction tx = null; +/// try { +/// tx = session.beginTransaction(); +/// //do some work +/// ... +/// tx.commit(); +/// } +/// catch (Exception e) { +/// if (tx!=null) tx.rollback(); +/// throw e; +/// } +/// } +/// ``` +/// +/// It's crucially important to appreciate the following restrictions and why they exist: +/// +/// - If the `Session` throws an exception, the current transaction must be rolled +/// back and the session must be discarded. The internal state of the `Session` +/// cannot be expected to be consistent with the database after the exception occurs. +/// - At the end of a logical transaction, the session must be explicitly +/// [destroyed][#close()], so that all JDBC resources may be released. +/// - If a transaction is rolled back, the state of the persistence context and of its +/// associated entities must be assumed inconsistent with the database, and the +/// session must be discarded. +/// - A `Session` is never thread-safe. It contains various different sorts of +/// fragile mutable state. Each thread or transaction must obtain its own dedicated +/// instance from the [SessionFactory]. +/// +/// An easy way to be sure that session and transaction management is being done correctly +/// is to [let the factory do it][SessionFactory#inTransaction(Consumer)]: +/// +/// ```java +/// sessionFactory.inTransaction(session -> { +/// //do the work +/// ... +/// }); +/// ``` +/// +/// A session may be used to [execute JDBC work][#doWork] using its JDBC +/// connection and transaction: +/// +/// ```java +/// session.doWork(connection -> { +/// try ( PreparedStatement ps = connection.prepareStatement(...) ) { +/// ps.execute(); +/// } +/// }); +/// ``` +/// +/// A `Session` instance is serializable if its entities are serializable. +/// +/// Every `Session` is a JPA [EntityManager]. Furthermore, when Hibernate is +/// acting as the JPA persistence provider, the method [#unwrap(Class)] +/// may be used to obtain the underlying `Session`. +/// +/// Hibernate, unlike JPA, allows a persistence unit where an entity class is mapped multiple +/// times, with different entity names, usually to different tables. In this case, the session +/// needs a way to identify the entity name of a given instance of the entity class. Therefore, +/// some operations of this interface, including operations inherited from `EntityManager`, +/// are overloaded with a form that accepts an explicit entity name along with the instance. An +/// alternative solution to this problem is to provide an [EntityNameResolver]. +/// +/// @see SessionFactory +/// +/// @author Gavin King +/// @author Steve Ebersole public interface Session extends SharedSessionContract, EntityManager { - /** - * Force this session to flush. Must be called at the end of a unit of work, - * before the transaction is committed. Depending on the current - * {@linkplain #setHibernateFlushMode(FlushMode) flush mode}, the session might - * automatically flush when {@link Transaction#commit()} is called, and it is not - * necessary to call this method directly. - *

- * Flushing is the process of synchronizing the underlying persistent - * store with persistable state held in memory. - * - * @throws HibernateException if changes could not be synchronized with the database - */ + /// Force this session to flush. Must be called at the end of a unit of work, + /// before the transaction is committed. Depending on the current + /// [flush mode][#getHibernateFlushMode()], the session might + /// automatically flush when [Transaction#commit()] is called, and it is not + /// necessary to call this method directly. + /// + /// _Flushing_ is the process of synchronizing the underlying persistent + /// store with persistable state held in memory. + /// + /// @throws HibernateException if changes could not be synchronized with the database @Override void flush(); - /** - * Set the current {@linkplain FlushModeType JPA flush mode} for this session. - *

- * Flushing is the process of synchronizing the underlying persistent - * store with persistable state held in memory. The current flush mode determines - * when the session is automatically flushed. - * - * @param flushMode the new {@link FlushModeType} - * - * @see #setHibernateFlushMode(FlushMode) - */ + /// Set the current [JPA flush mode][FlushModeType] for this session. + /// + /// _Flushing_ is the process of synchronizing the underlying persistent + /// store with persistable state held in memory. The current flush mode determines + /// when the session is automatically flushed. + /// + /// @param flushMode the new [FlushModeType] + /// + /// @see #setHibernateFlushMode(FlushMode) @Override void setFlushMode(FlushModeType flushMode); - /** - * Set the current {@linkplain FlushMode flush mode} for this session. - *

- * Flushing is the process of synchronizing the underlying persistent - * store with persistable state held in memory. The current flush mode determines - * when the session is automatically flushed. - *

- * The {@linkplain FlushMode#AUTO default flush mode} is sometimes unnecessarily - * aggressive. For a logically "read only" session, it's reasonable to set the - * session's flush mode to {@link FlushMode#MANUAL} at the start of the session - * in order to avoid some unnecessary work. - *

- * Note that {@link FlushMode} defines more options than {@link FlushModeType}. - * - * @param flushMode the new {@link FlushMode} - */ + /// Set the current [flush mode][FlushMode] for this session. + /// + /// _Flushing_ is the process of synchronizing the underlying persistent + /// store with persistable state held in memory. The current flush mode determines + /// when the session is automatically flushed. + /// + /// The [default flush mode][FlushMode#AUTO] is sometimes unnecessarily + /// aggressive. For a logically "read only" session, it's reasonable to set the + /// session's flush mode to [FlushMode#MANUAL] at the start of the session + /// in order to avoid some unnecessary work. + /// + /// Note that [FlushMode] defines more options than [FlushModeType]. + /// + /// @param flushMode the new [FlushMode] void setHibernateFlushMode(FlushMode flushMode); - /** - * Get the current {@linkplain FlushModeType JPA flush mode} for this session. - * - * @return the {@link FlushModeType} currently in effect - * - * @see #getHibernateFlushMode() - */ + /// Get the current [JPA flush mode][FlushModeType] for this session. + /// + /// @return the [FlushModeType] currently in effect + /// + /// @see #getHibernateFlushMode() @Override FlushModeType getFlushMode(); - /** - * Get the current {@linkplain FlushMode flush mode} for this session. - * - * @return the {@link FlushMode} currently in effect - */ + /// Get the current [flush mode][FlushMode] for this session. + /// + /// @return the [FlushMode] currently in effect FlushMode getHibernateFlushMode(); - /** - * Set the current {@linkplain CacheMode cache mode} for this session. - *

- * The cache mode determines the manner in which this session can interact with - * the second level cache. - * - * @param cacheMode the new cache mode - */ + /// Set the current [cache mode][CacheMode] for this session. + /// + /// The cache mode determines the manner in which this session can interact with + /// the second level cache. + /// + /// @param cacheMode the new cache mode void setCacheMode(CacheMode cacheMode); - /** - * Get the current {@linkplain CacheMode cache mode} for this session. - * - * @return the current cache mode - */ + /// Get the current [cache mode][CacheMode] for this session. + /// + /// @return the current cache mode CacheMode getCacheMode(); - /** - * The JPA-defined {@link CacheStoreMode}. - * - * @see #getCacheMode() - * - * @since 6.2 - */ + /// The JPA-defined [CacheStoreMode]. + /// + /// @see #getCacheMode() + /// + /// @since 6.2 @Override CacheStoreMode getCacheStoreMode(); - /** - * The JPA-defined {@link CacheRetrieveMode}. - * - * @see #getCacheMode() - * - * @since 6.2 - */ + /// The JPA-defined [CacheRetrieveMode]. + /// + /// @see #getCacheMode() + /// + /// @since 6.2 @Override CacheRetrieveMode getCacheRetrieveMode(); - /** - * Enable or disable writes to the second-level cache. - * - * @param cacheStoreMode a JPA-defined {@link CacheStoreMode} - * - * @see #setCacheMode(CacheMode) - * - * @since 6.2 - */ + /// Enable or disable writes to the second-level cache. + /// + /// @param cacheStoreMode a JPA-defined [CacheStoreMode] + /// + /// @see #setCacheMode(CacheMode) + /// + /// @since 6.2 @Override void setCacheStoreMode(CacheStoreMode cacheStoreMode); - /** - * Enable or disable reads from the second-level cache. - * - * @param cacheRetrieveMode a JPA-defined {@link CacheRetrieveMode} - * - * @see #setCacheMode(CacheMode) - * - * @since 6.2 - */ + /// Enable or disable reads from the second-level cache. + /// + /// @param cacheRetrieveMode a JPA-defined [CacheRetrieveMode] + /// + /// @see #setCacheMode(CacheMode) + /// + /// @since 6.2 @Override void setCacheRetrieveMode(CacheRetrieveMode cacheRetrieveMode); - /** - * Get the maximum batch size for batch fetching associations by - * id in this session. - * - * @since 6.3 - */ + /// Get the maximum batch size for batch fetching associations by + /// id in this session. + /// + /// @since 6.3 int getFetchBatchSize(); - /** - * Set the maximum batch size for batch fetching associations by - * id in this session. Override the - * {@linkplain org.hibernate.boot.spi.SessionFactoryOptions#getDefaultBatchFetchSize() - * factory-level} default controlled by the configuration property - * {@value org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE}. - *

- *

    - *
  • If {@code batchSize>1}, then batch fetching is enabled. - *
  • If {@code batchSize<0}, the batch size is inherited from - * the factory-level setting. - *
  • Otherwise, batch fetching is disabled. - *
- * - * @param batchSize the maximum batch size for batch fetching - * - * @since 6.3 - * - * @see org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE - */ + /// Set the maximum batch size for batch fetching associations by + /// id in this session. Override the + /// [factory-level][org.hibernate.boot.spi.SessionFactoryOptions#getDefaultBatchFetchSize()] + /// default controlled by the configuration property + /// {@value org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE}. + /// + /// - If `batchSize>1`, then batch fetching is enabled. + /// - If `batchSize<0`, the batch size is inherited from + /// the factory-level setting. + /// - Otherwise, batch fetching is disabled. + /// + /// @param batchSize the maximum batch size for batch fetching + /// + /// @since 6.3 + /// + /// @see org.hibernate.cfg.AvailableSettings#DEFAULT_BATCH_FETCH_SIZE void setFetchBatchSize(int batchSize); - /** - * Determine if subselect fetching is enabled in this session. - * - * @return {@code true} is subselect fetching is enabled - * - * @since 6.3 - */ + /// Determine if subselect fetching is enabled in this session. + /// + /// @return `true` is subselect fetching is enabled + /// + /// @since 6.3 boolean isSubselectFetchingEnabled(); - /** - * Enable or disable subselect fetching in this session. Override the - * {@linkplain org.hibernate.boot.spi.SessionFactoryOptions#isSubselectFetchEnabled() - * factory-level} default controlled by the configuration property - * {@value org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH}. - * - * @param enabled {@code true} to enable subselect fetching - * - * @since 6.3 - * - * @see org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH - */ + /// Enable or disable subselect fetching in this session. Override the + /// [factory-level][org.hibernate.boot.spi.SessionFactoryOptions#isSubselectFetchEnabled()] + /// default controlled by the configuration property + /// {@value org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH}. + /// + /// @param enabled `true` to enable subselect fetching + /// + /// @since 6.3 + /// + /// @see org.hibernate.cfg.AvailableSettings#USE_SUBSELECT_FETCH void setSubselectFetchingEnabled(boolean enabled); - /** - * Get the session factory which created this session. - * - * @return the session factory - * - * @see SessionFactory - */ + /// Get the session factory which created this session. + /// + /// @return the session factory + /// + /// @see SessionFactory SessionFactory getSessionFactory(); - /** - * Cancel the execution of the current query. - *

- * This is the sole method on session which may be safely called from - * another thread. - * - * @throws HibernateException if there was a problem cancelling the query - */ + /// Cancel the execution of the current query. + /// + /// This is the sole method on session which may be safely called from + /// another thread. + /// + /// @throws HibernateException if there was a problem cancelling the query void cancelQuery(); - /** - * Does this session contain any changes which must be synchronized with - * the database? In other words, would any DML operations be executed if - * we flushed this session? - * - * @return {@code true} if the session contains pending changes; - * {@code false} otherwise. - */ + /// Whether this session contains any changes which must be synchronized with + /// the database. In other words, would any DML operations be executed if + /// we flushed this session? + /// + /// @return `true` if the session contains pending changes; `false` otherwise. boolean isDirty(); - /** - * Will entities and proxies that are loaded into this session be made - * read-only by default? - *

- * To determine the read-only/modifiable setting for a particular entity - * or proxy use {@link #isReadOnly(Object)}. - * - * @see #isReadOnly(Object) - * - * @return {@code true}, loaded entities/proxies will be made read-only by default; - * {@code false}, loaded entities/proxies will be made modifiable by default. - */ + /// Will entities and proxies that are loaded into this session be made + /// read-only by default? + /// + /// To determine the read-only/modifiable setting for a particular entity + /// or proxy use [#isReadOnly(Object)]. + /// + /// @see #isReadOnly(Object) + /// + /// @return `true`, loaded entities/proxies will be made read-only by default; + /// `false`, loaded entities/proxies will be made modifiable by default. boolean isDefaultReadOnly(); - /** - * Change the default for entities and proxies loaded into this session - * from modifiable to read-only mode, or from read-only to modifiable mode. - *

- * Read-only entities are not dirty-checked, and snapshots of persistent - * state are not maintained. Read-only entities can be modified, but a - * modification to a field of a read-only entity is not made persistent. - *

- * When a proxy is initialized, the loaded entity will have the same - * read-only/modifiable setting as the uninitialized proxy, regardless of - * the {@linkplain #isDefaultReadOnly current default read-only mode} - * of the session. - *

- * To change the read-only/modifiable setting for a particular entity - * or proxy that already belongs to this session, use - * {@link #setReadOnly(Object, boolean)}. - *

- * To override the default read-only mode of the current session for - * all entities and proxies returned by a given {@code Query}, use - * {@link Query#setReadOnly(boolean)}. - *

- * Every instance of an {@linkplain org.hibernate.annotations.Immutable - * immutable} entity is loaded in read-only mode. - * - * @see #setReadOnly(Object,boolean) - * @see Query#setReadOnly(boolean) - * - * @param readOnly {@code true}, the default for loaded entities/proxies is read-only; - * {@code false}, the default for loaded entities/proxies is modifiable - * @throws SessionException if the session was originally - * {@linkplain SessionBuilder#readOnly created in read-only mode} - */ + /// Change the default for entities and proxies loaded into this session + /// from modifiable to read-only mode, or from read-only to modifiable mode. + /// + /// Read-only entities are not dirty-checked, and snapshots of persistent + /// state are not maintained. Read-only entities can be modified, but a + /// modification to a field of a read-only entity is not made persistent. + /// + /// When a proxy is initialized, the loaded entity will have the same + /// read-only/modifiable setting as the uninitialized proxy, regardless of + /// the [current default read-only mode][#isDefaultReadOnly] + /// of the session. + /// + /// To change the read-only/modifiable setting for a particular entity + /// or proxy that already belongs to this session, use + /// [#setReadOnly(Object,boolean)]. + /// + /// To override the default read-only mode of the current session for + /// all entities and proxies returned by a given `Query`, use + /// [Query#setReadOnly(boolean)]. + /// + /// Every instance of an [immutable][org.hibernate.annotations.Immutable] + /// entity is loaded in read-only mode. + /// + /// @see #setReadOnly(Object,boolean) + /// @see Query#setReadOnly(boolean) + /// + /// @param readOnly `true`, the default for loaded entities/proxies is read-only; + /// `false`, the default for loaded entities/proxies is modifiable + /// @throws SessionException if the session was originally + /// {@linkplain SessionBuilder#readOnly created in read-only mode} void setDefaultReadOnly(boolean readOnly); - /** - * Return the identifier value of the given entity associated with this session. - * An exception is thrown if the given entity instance is transient or detached - * in relation to this session. - * - * @param object a persistent instance associated with this session - * - * @return the identifier - * - * @throws TransientObjectException if the instance is transient or associated with - * a different session - */ + /// Return the identifier value of the given entity associated with this session. + /// An exception is thrown if the given entity instance is transient or detached + /// in relation to this session. + /// + /// @param object a persistent instance associated with this session + /// + /// @return the identifier + /// + /// @throws TransientObjectException if the instance is transient or associated with + /// a different session Object getIdentifier(Object object); - /** - * Determine if the given entity is associated with this session. - * - * @param entityName the entity name - * @param object an instance of a persistent class - * - * @return {@code true} if the given instance is associated with this {@code Session} - * - * @deprecated Use {@link #contains(Object)} instead. - */ + /// Determine if the given entity is associated with this session. + /// + /// @param entityName the entity name + /// @param object an instance of a persistent class + /// + /// @return `true` if the given instance is associated with this `Session` + /// + /// @deprecated Use [#contains(Object)] instead. @Deprecated(since = "7.2", forRemoval = true) boolean contains(String entityName, Object object); - /** - * Remove this instance from the session cache. Changes to the instance will - * not be synchronized with the database. This operation cascades to associated - * instances if the association is mapped with - * {@link jakarta.persistence.CascadeType#DETACH}. - * - * @param object the managed instance to detach - */ + /// Remove this instance from the session cache. Changes to the instance will + /// not be synchronized with the database. This operation cascades to associated + /// instances if the association is mapped with [jakarta.persistence.CascadeType#DETACH]. + /// + /// @param object the managed instance to detach @Override void detach(Object object); - /** - * Remove this instance from the session cache. Changes to the instance will - * not be synchronized with the database. This operation cascades to associated - * instances if the association is mapped with - * {@link jakarta.persistence.CascadeType#DETACH}. - *

- * This operation is a synonym for {@link #detach(Object)}. - * - * @param object the managed entity to evict - * - * @throws IllegalArgumentException if the given object is not an entity - */ + /// Remove this instance from the session cache. Changes to the instance will + /// not be synchronized with the database. This operation cascades to associated + /// instances if the association is mapped with [jakarta.persistence.CascadeType#DETACH]. + /// + /// This operation is a synonym for [#detach(Object)]. + /// + /// @param object the managed entity to evict + /// + /// @throws IllegalArgumentException if the given object is not an entity void evict(Object object); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. - *

- * The object returned by {@code get()} or {@code find()} is either an unproxied instance - * of the given entity class, or a fully-fetched proxy object. - *

- * This operation requests {@link LockMode#NONE}, that is, no lock, allowing the object - * to be retrieved from the cache without the cost of database access. However, if it is - * necessary to read the state from the database, the object will be returned with the - * lock mode {@link LockMode#READ}. - *

- * To bypass the {@linkplain Cache second-level cache}, and ensure that the state of the - * requested instance is read directly from the database, either: - *

    - *
  • call {@link #find(Class, Object, FindOption...)}, passing - * {@link CacheRetrieveMode#BYPASS} as an option, - *
  • call {@link #find(Class, Object, FindOption...)} with the explicit lock mode - * {@link LockMode#READ}, or - *
  • {@linkplain #setCacheRetrieveMode set the cache mode} to - * {@link CacheRetrieveMode#BYPASS} before calling this method. - *
- * - * @apiNote This operation is very similar to {@link #get(Class, Object)}. - * - * @param entityType the entity type - * @param id an identifier - * - * @return a fully-fetched persistent instance or null - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. + /// + /// The object returned by `get()` or `find()` is either an unproxied instance + /// of the given entity class, or a fully-fetched proxy object. + /// + /// This operation requests [LockMode#NONE], that is, no lock, allowing the object + /// to be retrieved from the cache without the cost of database access. However, if it is + /// necessary to read the state from the database, the object will be returned with the + /// lock mode [LockMode#READ]. + /// + /// To bypass the [second-level cache][Cache], and ensure that the state of the + /// requested instance is read directly from the database, either: + /// + /// - call [#find(Class,Object,FindOption...)], passing + /// [CacheRetrieveMode#BYPASS] as an option, + /// - call [#find(Class,Object,FindOption...)] with the explicit lock mode + /// [LockMode#READ], or + /// - {@linkplain #setCacheRetrieveMode set the cache mode} to + /// [CacheRetrieveMode#BYPASS] before calling this method. + /// + /// @apiNote This operation is very similar to [#get(Class,Object)]. + /// + /// @param entityType the entity type + /// @param id an identifier + /// + /// @return a fully-fetched persistent instance or null @Override T find(Class entityType, Object id); - /** - * Return the persistent instance of the named entity type with the given identifier, - * or null if there is no such persistent instance. - *

- * Differs from {@linkplain #find(Class, Object)} in that this form accepts - * the entity name of a {@linkplain org.hibernate.metamodel.RepresentationMode#MAP dynamic entity}. - * - * @see #find(Class, Object) - */ + /// {@inheritDoc} + /// + /// @implNote Note that Hibernate's implementation of this method can + /// also be used for loading an entity by its [natural-id][org.hibernate.annotations.NaturalId] + /// by passing [FindBy#NATURAL_ID] as a [FindOption] and the natural-id value as the `key` to load. + @Override + T find(Class entityClass, Object key, FindOption... options); + + /// Return the persistent instance of the named entity type with the given identifier, + /// or null if there is no such persistent instance. + /// + /// Differs from {@linkplain #find(Class, Object)} in that this form accepts + /// the entity name of a [dynamic entity][org.hibernate.metamodel.RepresentationMode#MAP]. + /// + /// @see #find(Class, Object) Object find(String entityName, Object primaryKey); - /** - * Return the persistent instance of the named entity type with the given identifier - * using the specified options, or null if there is no such persistent instance. - *

- * Differs from {@linkplain #find(Class, Object, FindOption...)} in that this form accepts - * the entity name of a {@linkplain org.hibernate.metamodel.RepresentationMode#MAP dynamic entity}. - * - * @see #find(Class, Object, FindOption...) - */ + /// Return the persistent instance of the named entity type with the given identifier + /// using the specified options, or null if there is no such persistent instance. + /// + /// Differs from [#find(Class, Object, FindOption...)] in that this form accepts + /// the entity name of a [dynamic entity][org.hibernate.metamodel.RepresentationMode#MAP]. + /// + /// @see #find(Class, Object, FindOption...) Object find(String entityName, Object primaryKey, FindOption... options); - /// Find an entity by [natural-id][org.hibernate.annotations.NaturalId]. - /// - /// @param entityType The type of entity to load. - /// @param naturalId The natural-id value. - /// @param options The options to apply to the find operation. - /// - /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. - T findByNaturalId(Class entityType, Object naturalId, FindOption... options); - - /// Find an entity by [natural-id][org.hibernate.annotations.NaturalId]. - /// - /// @param entityName The name of the entity type to load. - /// @param naturalId The natural-id value. - /// @param options The options to apply to the find operation. - /// - /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. - Object findByNaturalId(String entityName, Object naturalId, FindOption... options); - - /// Find multiple entities by [natural-id][org.hibernate.annotations.NaturalId]. - /// - /// @param entityType The type of entity to load. - /// @param naturalIds The natural-id values. - /// @param options The options to apply to the find operation. May contain [FindMultipleOption] values. - /// - /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. - List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options); - - /// Find multiple entities by [natural-id][org.hibernate.annotations.NaturalId]. - /// - /// @param entityName The name of the entity type to load. - /// @param naturalIds The natural-id values. - /// @param options The options to apply to the find operation. May contain [FindMultipleOption] values. - /// - /// @apiNote For non-aggregated composite natural-ids, consider leveraging a [org.hibernate.annotations.NaturalIdClass]. - List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options); - - /** - * Return the persistent instances of the given entity class with the given identifiers - * as a list. The position of an instance in the returned list matches the position of its - * identifier in the given list of identifiers, and the returned list contains a null value - * if there is no persistent instance matching a given identifier. If an instance is already - * associated with the session, that instance is returned. This method never returns an - * uninitialized instance. - *

- * Every object returned by {@code findMultiple()} is either an unproxied instance of the - * given entity class, or a fully-fetched proxy object. - *

- * This method accepts {@link BatchSize} as an option, allowing control over the number of - * records retrieved in a single database request. The performance impact of setting a batch - * size depends on whether a SQL array may be used to pass the list of identifiers to the - * database: - *

    - *
  • for databases which {@linkplain org.hibernate.dialect.Dialect#supportsStandardArrays - * support standard SQL arrays}, a smaller batch size might be extremely inefficient - * compared to a very large batch size or no batching at all, but - *
  • on the other hand, for databases with no SQL array type, a large batch size results - * in long SQL statements with many JDBC parameters. - *
- * - * @param entityType the entity type - * @param ids the list of identifiers - * @param options options, if any - * - * @return an ordered list of persistent instances, with null elements representing missing - * entities, whose positions in the list match the positions of their ids in the - * given list of identifiers - * - * @see FindMultipleOption - * - * @since 7.0 - */ + /// Return the persistent instances of the given entity class with the given identifiers + /// as a list. The position of an instance in the returned list matches the position of its + /// identifier in the given list of identifiers, and the returned list contains a null value + /// if there is no persistent instance matching a given identifier. If an instance is already + /// associated with the session, that instance is returned. This method never returns an + /// uninitialized instance. + /// + /// Every object returned by `findMultiple()` is either an unproxied instance of the + /// given entity class, or a fully-fetched proxy object. + /// + /// This method accepts [BatchSize] as an option, allowing control over the number of + /// records retrieved in a single database request. The performance impact of setting a batch + /// size depends on whether a SQL array may be used to pass the list of identifiers to the + /// database: + /// + /// - for databases which [support standard SQL arrays][org.hibernate.dialect.Dialect#supportsStandardArrays], + /// a smaller batch size might be extremely inefficient compared to a very large batch size or + /// no batching at all, but + /// - on the other hand, for databases with no SQL array type, a large batch size results + /// in long SQL statements with many JDBC parameters. + /// + /// @param entityType the entity type + /// @param ids the list of identifiers + /// @param options options, if any + /// + /// @return an ordered list of persistent instances, with null elements representing missing + /// entities, whose positions in the list match the positions of their ids in the + /// given list of identifiers + /// + /// @see FindMultipleOption + /// + /// @since 7.0 List findMultiple(Class entityType, List ids, FindOption... options); - /** - * Return the persistent instances of the root entity of the given {@link EntityGraph} - * with the given identifiers as a list, fetching the associations specified by the - * graph, which is interpreted as a {@linkplain org.hibernate.graph.GraphSemantic#LOAD - * load graph}. The position of an instance in the returned list matches the position of - * its identifier in the given list of identifiers, and the returned list contains a null - * value if there is no persistent instance matching a given identifier. If an instance - * is already associated with the session, that instance is returned. This method never - * returns an uninitialized instance. - *

- * Every object returned by {@code findMultiple()} is either an unproxied instance of the - * given entity class, or a fully-fetched proxy object. - *

- * This method accepts {@link BatchSize} as an option, allowing control over the number of - * records retrieved in a single database request. The performance impact of setting a batch - * size depends on whether a SQL array may be used to pass the list of identifiers to the - * database: - *

    - *
  • for databases which {@linkplain org.hibernate.dialect.Dialect#supportsStandardArrays - * support standard SQL arrays}, a smaller batch size might be extremely inefficient - * compared to a very large batch size or no batching at all, but - *
  • on the other hand, for databases with no SQL array type, a large batch size results - * in long SQL statements with many JDBC parameters. - *
- * - * @param entityGraph the entity graph interpreted as a load graph - * @param ids the list of identifiers - * @param options options, if any - * - * @return an ordered list of persistent instances, with null elements representing missing - * entities, whose positions in the list match the positions of their ids in the - * given list of identifiers - * - * @see FindMultipleOption - * - * @since 7.0 - */ + /// Return the persistent instances of the root entity of the given [EntityGraph] + /// with the given identifiers as a list, fetching the associations specified by the + /// graph, which is interpreted as a [load graph][org.hibernate.graph.GraphSemantic#LOAD]. + /// The position of an instance in the returned list matches the position of + /// its identifier in the given list of identifiers, and the returned list contains a null + /// value if there is no persistent instance matching a given identifier. If an instance + /// is already associated with the session, that instance is returned. This method never + /// returns an uninitialized instance. + /// + /// Every object returned by `findMultiple()` is either an unproxied instance of the + /// given entity class, or a fully-fetched proxy object. + /// + /// This method accepts [BatchSize] as an option, allowing control over the number of + /// records retrieved in a single database request. The performance impact of setting a batch + /// size depends on whether a SQL array may be used to pass the list of identifiers to the + /// database: + /// + /// - for databases which [support standard SQL arrays][org.hibernate.dialect.Dialect#supportsStandardArrays] + /// a smaller batch size might be extremely inefficient + /// compared to a very large batch size or no batching at all, but + /// - on the other hand, for databases with no SQL array type, a large batch size results + /// in long SQL statements with many JDBC parameters. + /// + /// @param entityGraph the entity graph interpreted as a load graph + /// @param ids the list of identifiers + /// @param options options, if any + /// + /// @return an ordered list of persistent instances, with null elements representing missing + /// entities, whose positions in the list match the positions of their ids in the + /// given list of identifiers + /// + /// @see FindMultipleOption + /// + /// @since 7.0 List findMultiple(EntityGraph entityGraph, List ids, FindOption... options); - /** - * Read the persistent state associated with the given identifier into the given - * transient instance. - * - * @param object a transient instance of an entity class - * @param id an identifier - */ + /// Read the persistent state associated with the given identifier into the given + /// transient instance. + /// + /// @param object a transient instance of an entity class + /// @param id an identifier void load(Object object, Object id); - /** - * Persist the state of the given detached instance, reusing the current - * identifier value. This operation cascades to associated instances if - * the association is mapped with - * {@link org.hibernate.annotations.CascadeType#REPLICATE}. - * - * @param object a detached instance of a persistent class - * @param replicationMode the replication mode to use - * - * @deprecated With no real replacement. For some use cases try - * {@link StatelessSession#upsert(Object)}. - */ + /// Persist the state of the given detached instance, reusing the current + /// identifier value. This operation cascades to associated instances if + /// the association is mapped with [org.hibernate.annotations.CascadeType#REPLICATE]. + /// + /// @param object a detached instance of a persistent class + /// @param replicationMode the replication mode to use + /// + /// @deprecated With no real replacement. For some use cases try [StatelessSession#upsert(Object)]. @Deprecated( since = "6.0" ) void replicate(Object object, ReplicationMode replicationMode); - /** - * Persist the state of the given detached instance, reusing the current - * identifier value. This operation cascades to associated instances if - * the association is mapped with - * {@link org.hibernate.annotations.CascadeType#REPLICATE}. - * - * @param entityName the entity name - * @param object a detached instance of a persistent class - * @param replicationMode the replication mode to use - * - * @deprecated With no real replacement. For some use cases try - * {@link StatelessSession#upsert(Object)}. - */ + /// Persist the state of the given detached instance, reusing the current + /// identifier value. This operation cascades to associated instances if + /// the association is mapped with [org.hibernate.annotations.CascadeType#REPLICATE]. + /// + /// @param entityName the entity name + /// @param object a detached instance of a persistent class + /// @param replicationMode the replication mode to use + /// + /// @deprecated With no real replacement. For some use cases try [StatelessSession#upsert(Object)]. @Deprecated( since = "6.0" ) void replicate(String entityName, Object object, ReplicationMode replicationMode) ; - /** - * Copy the state of the given object onto the persistent object with the same - * identifier. If there is no persistent instance currently associated with - * the session, it will be loaded. Return the persistent instance. If the - * given instance is unsaved, save a copy and return it as a newly persistent - * instance. The given instance does not become associated with the session. - * This operation cascades to associated instances if the association is mapped - * with {@link jakarta.persistence.CascadeType#MERGE}. - * - * @param object a detached instance with state to be copied - * - * @return an updated persistent instance - */ + /// Copy the state of the given object onto the persistent object with the same + /// identifier. If there is no persistent instance currently associated with + /// the session, it will be loaded. Return the persistent instance. If the + /// given instance is unsaved, save a copy and return it as a newly persistent + /// instance. The given instance does not become associated with the session. + /// This operation cascades to associated instances if the association is mapped + /// with [jakarta.persistence.CascadeType#MERGE]. + /// + /// @param object a detached instance with state to be copied + /// + /// @return an updated persistent instance @Override T merge(T object); - /** - * Copy the state of the given object onto the persistent object with the same - * identifier. If there is no persistent instance currently associated with - * the session, it will be loaded. Return the persistent instance. If the - * given instance is unsaved, save a copy and return it as a newly persistent - * instance. The given instance does not become associated with the session. - * This operation cascades to associated instances if the association is mapped - * with {@link jakarta.persistence.CascadeType#MERGE}. - * - * @param entityName the entity name - * @param object a detached instance with state to be copied - * - * @return an updated persistent instance - */ + /// Copy the state of the given object onto the persistent object with the same + /// identifier. If there is no persistent instance currently associated with + /// the session, it will be loaded. Return the persistent instance. If the + /// given instance is unsaved, save a copy and return it as a newly persistent + /// instance. The given instance does not become associated with the session. + /// This operation cascades to associated instances if the association is mapped + /// with [jakarta.persistence.CascadeType#MERGE]. + /// + /// @param entityName the entity name + /// @param object a detached instance with state to be copied + /// + /// @return an updated persistent instance T merge(String entityName, T object); - /** - * Copy the state of the given object onto the persistent object with the same - * identifier. If there is no persistent instance currently associated with - * the session, it is loaded using the given {@link EntityGraph}, which is - * interpreted as a load graph. Return the persistent instance. If the given - * instance is unsaved, save a copy and return it as a newly persistent instance. - * The given instance does not become associated with the session. This operation - * cascades to associated instances if the association is mapped with - * {@link jakarta.persistence.CascadeType#MERGE}. - * - * @param object a detached instance with state to be copied - * @param loadGraph an entity graph interpreted as a load graph - * - * @return an updated persistent instance - * - * @since 7.0 - */ + /// Copy the state of the given object onto the persistent object with the same + /// identifier. If there is no persistent instance currently associated with + /// the session, it is loaded using the given [EntityGraph], which is + /// interpreted as a load graph. Return the persistent instance. If the given + /// instance is unsaved, save a copy and return it as a newly persistent instance. + /// The given instance does not become associated with the session. This operation + /// cascades to associated instances if the association is mapped with + /// [jakarta.persistence.CascadeType#MERGE]. + /// + /// @param object a detached instance with state to be copied + /// @param loadGraph an entity graph interpreted as a load graph + /// + /// @return an updated persistent instance + /// + /// @since 7.0 T merge(T object, EntityGraph loadGraph); - /** - * Make a transient instance persistent and mark it for later insertion in the - * database. This operation cascades to associated instances if the association - * is mapped with {@link jakarta.persistence.CascadeType#PERSIST}. - *

- * For an entity with a {@linkplain jakarta.persistence.GeneratedValue generated} - * id, {@code persist()} ultimately results in generation of an identifier for - * the given instance. But this may happen asynchronously, when the session is - * {@linkplain #flush() flushed}, depending on the identifier generation strategy. - * - * @param object a transient instance to be made persistent - */ + /// Make a transient instance persistent and mark it for later insertion in the + /// database. This operation cascades to associated instances if the association + /// is mapped with [jakarta.persistence.CascadeType#PERSIST]. + /// + /// For entities with a [generated id][jakarta.persistence.GeneratedValue], + /// `persist()` ultimately results in generation of an identifier for the + /// given instance. But this may happen asynchronously, when the session is + /// [flushed][#flush()], depending on the identifier generation strategy. + /// + /// @param object a transient instance to be made persistent @Override void persist(Object object); - /** - * Make a transient instance persistent and mark it for later insertion in the - * database. This operation cascades to associated instances if the association - * is mapped with {@link jakarta.persistence.CascadeType#PERSIST}. - *

- * For entities with a {@link jakarta.persistence.GeneratedValue generated id}, - * {@code persist()} ultimately results in generation of an identifier for the - * given instance. But this may happen asynchronously, when the session is - * {@linkplain #flush() flushed}, depending on the identifier generation strategy. - * - * @param entityName the entity name - * @param object a transient instance to be made persistent - */ + /// Make a transient instance persistent and mark it for later insertion in the + /// database. This operation cascades to associated instances if the association + /// is mapped with [jakarta.persistence.CascadeType#PERSIST]. + /// + /// For entities with a [generated id][jakarta.persistence.GeneratedValue], + /// `persist()` ultimately results in generation of an identifier for the + /// given instance. But this may happen asynchronously, when the session is + /// [flushed][#flush()], depending on the identifier generation strategy. + /// + /// @param entityName the entity name + /// @param object a transient instance to be made persistent void persist(String entityName, Object object); - /** - * Obtain the specified lock level on the given managed instance associated - * with this session. This operation may be used to: - *

    - *
  • perform a version check on an entity read from the second-level cache - * by requesting {@link LockMode#READ}, - *
  • schedule a version check at transaction commit by requesting - * {@link LockMode#OPTIMISTIC}, - *
  • schedule a version increment at transaction commit by requesting - * {@link LockMode#OPTIMISTIC_FORCE_INCREMENT} - *
  • upgrade to a pessimistic lock with {@link LockMode#PESSIMISTIC_READ} - * or {@link LockMode#PESSIMISTIC_WRITE}, or - *
  • immediately increment the version of the given instance by requesting - * {@link LockMode#PESSIMISTIC_FORCE_INCREMENT}. - *
- *

- * If the requested lock mode is already held on the given entity, this - * operation has no effect. - *

- * This operation cascades to associated instances if the association is - * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. - *

- * The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED} - * are not legal arguments to {@code lock()}. - * - * @param object a persistent instance associated with this session - * @param lockMode the lock level - * - * @see #lock(Object, LockModeType) - */ + /// Obtain the specified lock level on the given managed instance associated + /// with this session. This operation may be used to: + /// + /// - perform a version check on an entity read from the second-level cache + /// by requesting [LockMode#READ], + /// - schedule a version check at transaction commit by requesting + /// [LockMode#OPTIMISTIC], + /// - schedule a version increment at transaction commit by requesting + /// [LockMode#OPTIMISTIC_FORCE_INCREMENT] + /// - upgrade to a pessimistic lock with [LockMode#PESSIMISTIC_READ] + /// or [LockMode#PESSIMISTIC_WRITE], or + /// - immediately increment the version of the given instance by requesting + /// [LockMode#PESSIMISTIC_FORCE_INCREMENT]. + /// + /// If the requested lock mode is already held on the given entity, this + /// operation has no effect. + /// + /// This operation cascades to associated instances if the association is + /// mapped with [org.hibernate.annotations.CascadeType#LOCK]. + /// + /// The modes [LockMode#WRITE] and [LockMode#UPGRADE_SKIPLOCKED] + /// are not legal arguments to `lock()`. + /// + /// @param object a persistent instance associated with this session + /// @param lockMode the lock level + /// + /// @see #lock(Object, LockModeType) void lock(Object object, LockMode lockMode); - /** - * Obtain the specified lock level on the given managed instance associated - * with this session, applying any other specified options. This operation may - * be used to: - *

    - *
  • perform a version check on an entity read from the second-level cache - * by requesting {@link LockMode#READ}, - *
  • schedule a version check at transaction commit by requesting - * {@link LockMode#OPTIMISTIC}, - *
  • schedule a version increment at transaction commit by requesting - * {@link LockMode#OPTIMISTIC_FORCE_INCREMENT} - *
  • upgrade to a pessimistic lock with {@link LockMode#PESSIMISTIC_READ} - * or {@link LockMode#PESSIMISTIC_WRITE}, or - *
  • immediately increment the version of the given instance by requesting - * {@link LockMode#PESSIMISTIC_FORCE_INCREMENT}. - *
- *

- * If the requested lock mode is already held on the given entity, this - * operation has no effect. - *

- * This operation cascades to associated instances if the association is - * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. - *

- * The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED} - * are not legal arguments to {@code lock()}. - * - * @param object a persistent instance associated with this session - * @param lockMode the lock level - * - * @see #lock(Object, LockModeType, LockOption...) - */ + /// Obtain the specified lock level on the given managed instance associated + /// with this session, applying any other specified options. This operation may + /// be used to: + /// + /// - perform a version check on an entity read from the second-level cache + /// by requesting [LockMode#READ], + /// - schedule a version check at transaction commit by requesting + /// [LockMode#OPTIMISTIC], + /// - schedule a version increment at transaction commit by requesting + /// [LockMode#OPTIMISTIC_FORCE_INCREMENT] + /// - upgrade to a pessimistic lock with [LockMode#PESSIMISTIC_READ] + /// or [LockMode#PESSIMISTIC_WRITE], or + /// - immediately increment the version of the given instance by requesting + /// [LockMode#PESSIMISTIC_FORCE_INCREMENT]. + /// + /// If the requested lock mode is already held on the given entity, this + /// operation has no effect. + /// + /// This operation cascades to associated instances if the association is + /// mapped with [org.hibernate.annotations.CascadeType#LOCK]. + /// + /// The modes [LockMode#WRITE] and [LockMode#UPGRADE_SKIPLOCKED] + /// are not legal arguments to `lock()`. + /// + /// @param object a persistent instance associated with this session + /// @param lockMode the lock level + /// + /// @see #lock(Object, LockModeType, LockOption...) void lock(Object object, LockMode lockMode, LockOption... lockOptions); - /** - * Reread the state of the given managed instance associated with this session - * from the underlying database. This may be useful: - *

    - *
  • when a database trigger alters the object state upon insert or update, - *
  • after {@linkplain #createMutationQuery(String) executing} any HQL update - * or delete statement, - *
  • after {@linkplain #createNativeMutationQuery(String) executing} a native - * SQL statement, or - *
  • after inserting a {@link java.sql.Blob} or {@link java.sql.Clob}. - *
- *

- * This operation cascades to associated instances if the association is mapped - * with {@link jakarta.persistence.CascadeType#REFRESH}. - *

- * This operation requests {@link LockMode#READ}. To obtain a stronger lock, - * call {@link #refresh(Object, RefreshOption...)}, passing the appropriate - * {@link LockMode} as an option. - * - * @param object a persistent instance associated with this session - */ + /// Reread the state of the given managed instance associated with this session + /// from the underlying database. This may be useful: + /// + /// - when a database trigger alters the object state upon insert or update, + /// - after {@linkplain #createMutationQuery(String) executing} any HQL update + /// or delete statement, + /// - after {@linkplain #createNativeMutationQuery(String) executing} a native + /// SQL statement, or + /// - after inserting a [java.sql.Blob] or [java.sql.Clob]. + /// + /// This operation cascades to associated instances if the association is mapped + /// with [jakarta.persistence.CascadeType#REFRESH]. + /// + /// This operation requests [LockMode#READ]. To obtain a stronger lock, + /// call [#refresh(Object,RefreshOption...)], passing the appropriate + /// [LockMode] as an option. + /// + /// @param object a persistent instance associated with this session @Override void refresh(Object object); - /** - * Mark a persistence instance associated with this session for removal from - * the underlying database. Ths operation cascades to associated instances if - * the association is mapped {@link jakarta.persistence.CascadeType#REMOVE}. - *

- * Except when operating in fully JPA-compliant mode, this operation does, - * contrary to the JPA specification, accept a detached entity instance. - * - * @param object the managed persistent instance to remove, or a detached - * instance unless operating in fully JPA-compliant mode - */ + /// Mark a persistence instance associated with this session for removal from + /// the underlying database. Ths operation cascades to associated instances if + /// the association is mapped [jakarta.persistence.CascadeType#REMOVE]. + /// + /// Except when operating in fully JPA-compliant mode, this operation does, + /// contrary to the JPA specification, accept a detached entity instance. + /// + /// @param object the managed persistent instance to remove, or a detached + /// instance unless operating in fully JPA-compliant mode @Override void remove(Object object); - /** - * Determine the current {@linkplain LockMode lock mode} held on the given - * managed instance associated with this session. - *

- * Unlike the JPA-standard {@link #getLockMode}, this operation may be - * called when no transaction is active, in which case it should return - * {@link LockMode#NONE}, indicating that no pessimistic lock is held on - * the given entity. - * - * @param object a persistent instance associated with this session - * - * @return the lock mode currently held on the given entity - * - * @throws IllegalStateException if the given instance is not associated - * with this persistence context - * @throws ObjectDeletedException if the given instance was already - * {@linkplain #remove removed} - */ + /// Determine the current [lock mode][LockMode] held on the given + /// managed instance associated with this session. + /// + /// Unlike the JPA-standard [#getLockMode], this operation may be + /// called when no transaction is active, in which case it should return + /// [LockMode#NONE], indicating that no pessimistic lock is held on + /// the given entity. + /// + /// @param object a persistent instance associated with this session + /// + /// @return the lock mode currently held on the given entity + /// + /// @throws IllegalStateException if the given instance is not associated + /// with this persistence context + /// @throws ObjectDeletedException if the given instance was already + /// {@linkplain #remove removed} LockMode getCurrentLockMode(Object object); - /** - * Completely clear the persistence context. Evict all loaded instances, - * causing every managed entity currently associated with this session to - * transition to the detached state, and cancel all pending insertions, - * updates, and deletions. - *

- * Does not close open iterators or instances of {@link ScrollableResults}. - */ + /// Completely clear the persistence context. Evict all loaded instances, + /// causing every managed entity currently associated with this session to + /// transition to the detached state, and cancel all pending insertions, + /// updates, and deletions. + /// + /// Does not close open iterators or instances of [ScrollableResults]. @Override void clear(); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. - *

- * The object returned by {@code get()} or {@code find()} is either an unproxied instance - * of the given entity class, or a fully-fetched proxy object. - *

- * This operation requests {@link LockMode#NONE}, that is, no lock, allowing the object - * to be retrieved from the cache without the cost of database access. However, if it is - * necessary to read the state from the database, the object will be returned with the - * lock mode {@link LockMode#READ}. - *

- * To bypass the second-level cache, and ensure that the state is read from the database, - * either: - *

    - *
  • call {@link #get(Class, Object, LockMode)} with the explicit lock mode - * {@link LockMode#READ}, or - *
  • {@linkplain #setCacheMode set the cache mode} to {@link CacheMode#IGNORE} - * before calling this method. - *
- * - * @apiNote This operation is very similar to {@link #find(Class, Object)}. - * - * @param entityType the entity type - * @param id an identifier - * - * @return a persistent instance or null - * - * @deprecated Because the semantics of this method may change in a future release. - * Use {@link #find(Class, Object)} instead. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. + /// + /// The object returned by `get()` or `find()` is either an unproxied instance + /// of the given entity class, or a fully-fetched proxy object. + /// + /// This operation requests [LockMode#NONE], that is, no lock, allowing the object + /// to be retrieved from the cache without the cost of database access. However, if it is + /// necessary to read the state from the database, the object will be returned with the + /// lock mode [LockMode#READ]. + /// + /// To bypass the second-level cache, and ensure that the state is read from the database, + /// either: + /// + /// - call [#get(Class,Object,LockMode)] with the explicit lock mode + /// [LockMode#READ], or + /// - {@linkplain #setCacheMode set the cache mode} to [CacheMode#IGNORE] + /// before calling this method. + /// + /// @apiNote This operation is very similar to [#find(Class,Object)]. + /// + /// @param entityType the entity type + /// @param id an identifier + /// + /// @return a persistent instance or null + /// + /// @deprecated Because the semantics of this method may change in a future release. + /// Use [#find(Class,Object)] instead. @Deprecated(since = "7.0", forRemoval = true) T get(Class entityType, Object id); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @apiNote This operation is very similar to {@link #find(Class, Object, LockModeType)}. - * - * @param entityType the entity type - * @param id an identifier - * @param lockMode the lock mode - * - * @return a persistent instance or null - * - * @deprecated Use {@link #find(Class, Object, FindOption...)} instead. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @apiNote This operation is very similar to [#find(Class,Object,LockModeType)]. + /// + /// @param entityType the entity type + /// @param id an identifier + /// @param lockMode the lock mode + /// + /// @return a persistent instance or null + /// + /// @deprecated Use [#find(Class,Object,FindOption...)] instead. @Deprecated(since = "7.0", forRemoval = true) T get(Class entityType, Object id, LockMode lockMode); - /** - * Return the persistent instance of the given named entity with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. - * - * @param entityName the entity name - * @param id an identifier - * - * @return a persistent instance or null - * - * @deprecated The semantics of this method may change in a future release. - * Use {@link SessionFactory#createGraphForDynamicEntity(String)} - * together with {@link #find(EntityGraph, Object, FindOption...)} - * to load {@link org.hibernate.metamodel.RepresentationMode#MAP - * dynamic entities}. - * - * @see SessionFactory#createGraphForDynamicEntity(String) - * @see #find(EntityGraph, Object, FindOption...) - */ + /// Return the persistent instance of the given named entity with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. + /// + /// @param entityName the entity name + /// @param id an identifier + /// + /// @return a persistent instance or null + /// + /// @deprecated The semantics of this method may change in a future release. + /// Use [SessionFactory#createGraphForDynamicEntity(String)] + /// together with [#find(EntityGraph,Object,FindOption...)] + /// to load [dynamic entities][org.hibernate.metamodel.RepresentationMode#MAP]. + /// + /// @see SessionFactory#createGraphForDynamicEntity(String) + /// @see #find(EntityGraph, Object, FindOption...) @Deprecated(since = "7", forRemoval = true) Object get(String entityName, Object id); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @param entityName the entity name - * @param id an identifier - * @param lockMode the lock mode - * - * @return a persistent instance or null - * - * @see #get(String, Object, LockOptions) - * - * @deprecated The semantics of this method may change in a future release. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @param entityName the entity name + /// @param id an identifier + /// @param lockMode the lock mode + /// + /// @return a persistent instance or null + /// + /// @see #get(String, Object, LockOptions) + /// + /// @deprecated The semantics of this method may change in a future release. @Deprecated(since = "7.0", forRemoval = true) Object get(String entityName, Object id, LockMode lockMode); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @param entityType the entity type - * @param id an identifier - * @param lockOptions the lock mode - * - * @return a persistent instance or null - * - * @deprecated This method will be removed. - * Use {@link #find(Class, Object, FindOption...)} instead. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @param entityType the entity type + /// @param id an identifier + /// @param lockOptions the lock mode + /// + /// @return a persistent instance or null + /// + /// @deprecated This method will be removed. + /// Use [#find(Class,Object,FindOption...)] instead. @Deprecated(since = "7.0", forRemoval = true) T get(Class entityType, Object id, LockOptions lockOptions); - /** - * Return the persistent instance of the given entity class with the given identifier, - * or null if there is no such persistent instance. If the instance is already associated - * with the session, return that instance. This method never returns an uninitialized - * instance. Obtain the specified lock mode if the instance exists. - * - * @param entityName the entity name - * @param id an identifier - * @param lockOptions contains the lock mode - * - * @return a persistent instance or null - * - * @deprecated This method will be removed. - * Use {@link SessionFactory#createGraphForDynamicEntity(String)} - * together with {@link #find(EntityGraph, Object, FindOption...)} - * to load {@link org.hibernate.metamodel.RepresentationMode#MAP - * dynamic entities}. - */ + /// Return the persistent instance of the given entity class with the given identifier, + /// or null if there is no such persistent instance. If the instance is already associated + /// with the session, return that instance. This method never returns an uninitialized + /// instance. Obtain the specified lock mode if the instance exists. + /// + /// @param entityName the entity name + /// @param id an identifier + /// @param lockOptions contains the lock mode + /// + /// @return a persistent instance or null + /// + /// @deprecated This method will be removed. + /// Use [SessionFactory#createGraphForDynamicEntity(String)] + /// together with [#find(EntityGraph,Object,FindOption...)] + /// to load [dynamic entities][org.hibernate.metamodel.RepresentationMode#MAP]. @Deprecated(since = "7.0", forRemoval = true) Object get(String entityName, Object id, LockOptions lockOptions); - /** - * Obtain a lock on the given managed instance associated with this session, - * using the given {@linkplain LockOptions lock options}. - *

- * This operation cascades to associated instances if the association is - * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. - * - * @param object a persistent instance associated with this session - * @param lockOptions the lock options - * - * @since 6.2 - * - * @deprecated This method will be removed. - * Use {@linkplain #lock(Object, LockModeType, LockOption...)} instead - */ + /// Obtain a lock on the given managed instance associated with this session, + /// using the given [lock options][LockOptions]. + /// + /// This operation cascades to associated instances if the association is + /// mapped with [org.hibernate.annotations.CascadeType#LOCK]. + /// + /// @param object a persistent instance associated with this session + /// @param lockOptions the lock options + /// + /// @since 6.2 + /// + /// @deprecated This method will be removed. + /// Use [#lock(Object, LockModeType, LockOption...)] instead @Deprecated(since = "7.0", forRemoval = true) void lock(Object object, LockOptions lockOptions); - /** - * Reread the state of the given managed instance from the underlying database, - * obtaining the given {@link LockMode}. - * - * @param object a persistent instance associated with this session - * @param lockOptions contains the lock mode to use - * - * @deprecated This method will be removed. - * Use {@linkplain #refresh(Object, RefreshOption...)} instead - */ + /// Reread the state of the given managed instance from the underlying database, + /// obtaining the given [LockMode]. + /// + /// @param object a persistent instance associated with this session + /// @param lockOptions contains the lock mode to use + /// + /// @deprecated This method will be removed. + /// Use [#refresh(Object, RefreshOption...)] instead @Deprecated(since = "7.0", forRemoval = true) void refresh(Object object, LockOptions lockOptions); - /** - * Return the entity name for the given persistent entity. - *

- * If the given entity is an uninitialized proxy, the proxy is initialized by - * side effect. - * - * @param object a persistent entity associated with this session - * - * @return the entity name - */ + /// Return the entity name for the given persistent entity. + /// + /// If the given entity is an uninitialized proxy, the proxy is initialized by + /// side effect. + /// + /// @param object a persistent entity associated with this session + /// + /// @return the entity name String getEntityName(Object object); - /** - * Return a reference to the persistent instance with the given class and identifier, - * making the assumption that the instance is still persistent in the database. This - * method never results in access to the underlying data store, and thus might return - * a proxy that is initialized on-demand, when a non-identifier method is accessed. - *

- * Note that {@link Hibernate#createDetachedProxy(SessionFactory, Class, Object)} - * may be used to obtain a detached reference. - *

- * It's sometimes necessary to narrow a reference returned by {@code getReference()} - * to a subtype of the given entity type. A direct Java typecast should never be used - * in this situation. Instead, the method {@link Hibernate#unproxy(Object, Class)} is - * the recommended way to narrow the type of a proxy object. Alternatively, a new - * reference may be obtained by simply calling {@code getReference()} again, passing - * the subtype. Either way, the narrowed reference will usually not be identical to - * the original reference, when the references are compared using the {@code ==} - * operator. - * - * @param entityType the entity type - * @param id the identifier of a persistent instance that exists in the database - * - * @return the persistent instance or proxy - */ + /// Return a reference to the persistent instance with the given class and identifier, + /// making the assumption that the instance is still persistent in the database. This + /// method never results in access to the underlying data store, and thus might return + /// a proxy that is initialized on-demand, when a non-identifier method is accessed. + /// + /// Note that [Hibernate#createDetachedProxy(SessionFactory,Class,Object)] + /// may be used to obtain a _detached_ reference. + /// + /// It's sometimes necessary to narrow a reference returned by `getReference()` + /// to a subtype of the given entity type. A direct Java typecast should never be used + /// in this situation. Instead, the method [Hibernate#unproxy(Object,Class)] is + /// the recommended way to narrow the type of a proxy object. Alternatively, a new + /// reference may be obtained by simply calling `getReference()` again, passing + /// the subtype. Either way, the narrowed reference will usually not be identical to + /// the original reference, when the references are compared using the `==` + /// operator. + /// + /// @param entityType the entity type + /// @param id the identifier of a persistent instance that exists in the database + /// + /// @return the persistent instance or proxy @Override T getReference(Class entityType, Object id); - /** - * Return a reference to the persistent instance of the given named entity with the - * given identifier, making the assumption that the instance is still persistent in - * the database. This method never results in access to the underlying data store, - * and thus might return a proxy that is initialized on-demand, when a non-identifier - * method is accessed. - * - * @param entityName the entity name - * @param id the identifier of a persistent instance that exists in the database - * - * @return the persistent instance or proxy - */ + /// Return a reference to the persistent instance of the given named entity with the + /// given identifier, making the assumption that the instance is still persistent in + /// the database. This method never results in access to the underlying data store, + /// and thus might return a proxy that is initialized on-demand, when a non-identifier + /// method is accessed. + /// + /// @param entityName the entity name + /// @param id the identifier of a persistent instance that exists in the database + /// + /// @return the persistent instance or proxy Object getReference(String entityName, Object id); - /** - * Return a reference to the persistent instance with the same identity as the given - * instance, which might be detached, making the assumption that the instance is still - * persistent in the database. This method never results in access to the underlying - * data store, and thus might return a proxy that is initialized on-demand, when a - * non-identifier method is accessed. - * - * @param object a detached persistent instance - * - * @return the persistent instance or proxy - * - * @since 6.0 - */ + /// Return a reference to the persistent instance with the same identity as the given + /// instance, which might be detached, making the assumption that the instance is still + /// persistent in the database. This method never results in access to the underlying + /// data store, and thus might return a proxy that is initialized on-demand, when a + /// non-identifier method is accessed. + /// + /// @param object a detached persistent instance + /// + /// @return the persistent instance or proxy + /// + /// @since 6.0 @Override T getReference(T object); - /** - * Create an {@link IdentifierLoadAccess} instance to retrieve an instance of the given - * entity type by its primary key. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link IdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity - * - * @deprecated This method will be removed. - * Use {@link #find(Class, Object, FindOption...)} instead. - * See {@link FindOption}. - */ + /// Create an [IdentifierLoadAccess] instance to retrieve an instance of the given + /// entity type by its primary key. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [IdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity + /// + /// @deprecated This method will be removed. + /// Use [#find(Class,Object,FindOption...)] instead. + /// See [FindOption]. @Deprecated(since = "7.1", forRemoval = true) IdentifierLoadAccess byId(Class entityClass); - /** - * Create an {@link IdentifierLoadAccess} instance to retrieve an instance of the named - * entity type by its primary key. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link IdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity - * - * @deprecated This method will be removed. - * Use {@link #find(String, Object, FindOption...)} instead. - * See {@link FindOption}. - */ + /// Create an [IdentifierLoadAccess] instance to retrieve an instance of the named + /// entity type by its primary key. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [IdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity + /// + /// @deprecated This method will be removed. + /// Use [#find(String,Object,FindOption...)] instead. + /// See [FindOption]. @Deprecated(since = "7.1", forRemoval = true) IdentifierLoadAccess byId(String entityName); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the given entity type by their primary key values, using batching. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link MultiIdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity - * - * @see #findMultiple(Class, List, FindOption...) - * - * @deprecated Use {@link #findMultiple(Class, List, FindOption...)} instead. - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the given entity type by their primary key values, using batching. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [MultiIdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity + /// + /// @see #findMultiple(Class, List, FindOption...) + /// + /// @deprecated Use [#findMultiple(Class,List,FindOption...)] instead. @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(Class entityClass); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the named entity type by their primary key values, using batching. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link MultiIdentifierLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity - * - * @deprecated Use {@link #findMultiple(EntityGraph, List, FindOption...)} instead, - * with {@linkplain SessionFactory#createGraphForDynamicEntity(String)}. - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the named entity type by their primary key values, using batching. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [MultiIdentifierLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity + /// + /// @deprecated Use [#findMultiple(EntityGraph,List,FindOption...)] instead, + /// with {@linkplain SessionFactory#createGraphForDynamicEntity(String)}. @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(String entityName); - /** - * Create a {@link NaturalIdLoadAccess} instance to retrieve an instance of the given - * entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which may be a composite natural id. The entity must have at least one attribute - * annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link NaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity, - * or if the entity does not declare a natural id - * - * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. - */ + /// Create a [NaturalIdLoadAccess] instance to retrieve an instance of the given + /// entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which may be a composite natural id. The entity must have at least one attribute + /// annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [NaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [FindBy#NATURAL_ID] instead. @Deprecated NaturalIdLoadAccess byNaturalId(Class entityClass); - /** - * Create a {@link NaturalIdLoadAccess} instance to retrieve an instance of the named - * entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which may be a composite natural id. The entity must have at least one attribute - * annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link NaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity, - * or if the entity does not declare a natural id - * - * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. - */ + /// Create a [NaturalIdLoadAccess] instance to retrieve an instance of the named + /// entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which may be a composite natural id. The entity must have at least one attribute + /// annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [NaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [FindBy#NATURAL_ID] instead. @Deprecated NaturalIdLoadAccess byNaturalId(String entityName); - /** - * Create a {@link SimpleNaturalIdLoadAccess} instance to retrieve an instance of the - * given entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which must be a simple (non-composite) value. The entity must have exactly one - * attribute annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link SimpleNaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity, - * or if the entity does not declare a natural id - * - * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. - */ + /// Create a [SimpleNaturalIdLoadAccess] instance to retrieve an instance of the + /// given entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which must be a simple (non-composite) value. The entity must have exactly one + /// attribute annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [SimpleNaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [FindBy#NATURAL_ID] instead. @Deprecated SimpleNaturalIdLoadAccess bySimpleNaturalId(Class entityClass); - /** - * Create a {@link SimpleNaturalIdLoadAccess} instance to retrieve an instance of the - * named entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, - * which must be a simple (non-composite) value. The entity must have exactly one - * attribute annotated {@link org.hibernate.annotations.NaturalId}. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link SimpleNaturalIdLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity, - * or if the entity does not declare a natural id - * - * @deprecated (since 7.3) : Use {@linkplain #findByNaturalId} instead. - */ + /// Create a [SimpleNaturalIdLoadAccess] instance to retrieve an instance of the + /// named entity type by its {@linkplain org.hibernate.annotations.NaturalId natural id}, + /// which must be a simple (non-composite) value. The entity must have exactly one + /// attribute annotated [org.hibernate.annotations.NaturalId]. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [SimpleNaturalIdLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #find} with [FindBy#NATURAL_ID] instead. @Deprecated SimpleNaturalIdLoadAccess bySimpleNaturalId(String entityName); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the given entity type by their by {@linkplain org.hibernate.annotations.NaturalId - * natural id} values, using batching. - * - * @param entityClass the entity type to be retrieved - * - * @return an instance of {@link NaturalIdMultiLoadAccess} for executing the lookup - * - * @throws HibernateException If the given class does not resolve as a mapped entity, - * or if the entity does not declare a natural id - * - * @deprecated (since 7.3) : Use {@linkplain #findMultipleByNaturalId} instead. - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the given entity type by their by {@linkplain org.hibernate.annotations.NaturalId + /// natural id} values, using batching. + /// + /// @param entityClass the entity type to be retrieved + /// + /// @return an instance of [NaturalIdMultiLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given class does not resolve as a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [FindBy#NATURAL_ID] instead. + /// @Deprecated NaturalIdMultiLoadAccess byMultipleNaturalId(Class entityClass); - /** - * Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple instances - * of the named entity type by their by {@linkplain org.hibernate.annotations.NaturalId - * natural id} values, using batching. - * - * @param entityName the entity name of the entity type to be retrieved - * - * @return an instance of {@link NaturalIdMultiLoadAccess} for executing the lookup - * - * @throws HibernateException If the given name does not resolve to a mapped entity, - * or if the entity does not declare a natural id - * - * @deprecated (since 7.3) : Use {@linkplain #findMultipleByNaturalId} instead. - */ + /// Create a [MultiIdentifierLoadAccess] instance to retrieve multiple instances + /// of the named entity type by their by {@linkplain org.hibernate.annotations.NaturalId + /// natural id} values, using batching. + /// + /// @param entityName the entity name of the entity type to be retrieved + /// + /// @return an instance of [NaturalIdMultiLoadAccess] for executing the lookup + /// + /// @throws HibernateException If the given name does not resolve to a mapped entity, + /// or if the entity does not declare a natural id + /// + /// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [FindBy#NATURAL_ID] instead. @Deprecated NaturalIdMultiLoadAccess byMultipleNaturalId(String entityName); - /** - * Get the {@linkplain SessionStatistics statistics} for this session. - * - * @return the session statistics being collected for this session - */ + /// Get the [statistics][SessionStatistics] for this session. + /// + /// @return the session statistics being collected for this session SessionStatistics getStatistics(); - /** - * Is the specified entity or proxy read-only? - *

- * To get the default read-only/modifiable setting used for - * entities and proxies that are loaded into the session use - * {@link #isDefaultReadOnly()} - * - * @see #isDefaultReadOnly() - * - * @param entityOrProxy an entity or proxy - * @return {@code true} if the entity or proxy is read-only, - * {@code false} if the entity or proxy is modifiable. - */ + /// Is the specified entity or proxy read-only? + /// + /// To get the default read-only/modifiable setting used for + /// entities and proxies that are loaded into the session use + /// [#isDefaultReadOnly()] + /// + /// @see #isDefaultReadOnly() + /// + /// @param entityOrProxy an entity or proxy + /// @return `true` if the entity or proxy is read-only, + /// `false` if the entity or proxy is modifiable. boolean isReadOnly(Object entityOrProxy); - /** - * Set an unmodified persistent object to read-only mode, or a read-only - * object to modifiable mode. In read-only mode, no snapshot is maintained, - * the instance is never dirty-checked, and mutations to the fields of the - * entity are not made persistent. - *

- * If the entity or proxy already has the specified read-only/modifiable - * setting, then this method does nothing. - *

- * To set the default read-only/modifiable setting used for all entities - * and proxies that are loaded into the session use - * {@link #setDefaultReadOnly(boolean)}. - *

- * To override the default read-only mode of the current session for - * all entities and proxies returned by a given {@code Query}, use - * {@link Query#setReadOnly(boolean)}. - *

- * Every instance of an {@linkplain org.hibernate.annotations.Immutable - * immutable} entity is loaded in read-only mode. An immutable entity may - * not be set to modifiable. - * - * @see #setDefaultReadOnly(boolean) - * @see Query#setReadOnly(boolean) - * @see IdentifierLoadAccess#withReadOnly(boolean) - * @see org.hibernate.annotations.Immutable - * - * @param entityOrProxy an entity or proxy - * @param readOnly {@code true} if the entity or proxy should be made read-only; - * {@code false} if the entity or proxy should be made modifiable - * - * @throws IllegalStateException if an immutable entity is set to modifiable - */ + /// Set an unmodified persistent object to read-only mode, or a read-only + /// object to modifiable mode. In read-only mode, no snapshot is maintained, + /// the instance is never dirty-checked, and mutations to the fields of the + /// entity are not made persistent. + /// + /// If the entity or proxy already has the specified read-only/modifiable + /// setting, then this method does nothing. + /// + /// To set the default read-only/modifiable setting used for all entities + /// and proxies that are loaded into the session use + /// [#setDefaultReadOnly(boolean)]. + /// + /// To override the default read-only mode of the current session for + /// all entities and proxies returned by a given `Query`, use + /// [Query#setReadOnly(boolean)]. + /// + /// Every instance of an [immutable][org.hibernate.annotations.Immutable] + /// entity is loaded in read-only mode. An immutable entity may + /// not be set to modifiable. + /// + /// @see #setDefaultReadOnly(boolean) + /// @see Query#setReadOnly(boolean) + /// @see IdentifierLoadAccess#withReadOnly(boolean) + /// @see org.hibernate.annotations.Immutable + /// + /// @param entityOrProxy an entity or proxy + /// @param readOnly `true` if the entity or proxy should be made read-only; + /// `false` if the entity or proxy should be made modifiable + /// + /// @throws IllegalStateException if an immutable entity is set to modifiable void setReadOnly(Object entityOrProxy, boolean readOnly); - /** - * Is the {@link org.hibernate.annotations.FetchProfile fetch profile} - * with the given name enabled in this session? - * - * @param name the name of the profile - * @return True if fetch profile is enabled; false if not. - * - * @throws UnknownProfileException Indicates that the given name does not - * match any known fetch profile names - * - * @see org.hibernate.annotations.FetchProfile - */ + /// Is the [fetch profile][org.hibernate.annotations.FetchProfile] + /// with the given name enabled in this session? + /// + /// @param name the name of the profile + /// @return True if fetch profile is enabled; false if not. + /// + /// @throws UnknownProfileException Indicates that the given name does not + /// match any known fetch profile names + /// + /// @see org.hibernate.annotations.FetchProfile boolean isFetchProfileEnabled(String name) throws UnknownProfileException; - /** - * Enable the {@link org.hibernate.annotations.FetchProfile fetch profile} - * with the given name in this session. If the requested fetch profile is - * already enabled, the call has no effect. - * - * @param name the name of the fetch profile to be enabled - * - * @throws UnknownProfileException Indicates that the given name does not - * match any known fetch profile names - * - * @see org.hibernate.annotations.FetchProfile - */ + /// Enable the [fetch profile][org.hibernate.annotations.FetchProfile] + /// with the given name in this session. If the requested fetch profile is + /// already enabled, the call has no effect. + /// + /// @param name the name of the fetch profile to be enabled + /// + /// @throws UnknownProfileException Indicates that the given name does not + /// match any known fetch profile names + /// + /// @see org.hibernate.annotations.FetchProfile void enableFetchProfile(String name) throws UnknownProfileException; - /** - * Disable the {@link org.hibernate.annotations.FetchProfile fetch profile} - * with the given name in this session. If the requested fetch profile is - * not currently enabled, the call has no effect. - * - * @param name the name of the fetch profile to be disabled - * - * @throws UnknownProfileException Indicates that the given name does not - * match any known fetch profile names - * - * @see org.hibernate.annotations.FetchProfile - */ + /// Disable the [fetch profile][org.hibernate.annotations.FetchProfile] + /// with the given name in this session. If the requested fetch profile is + /// not currently enabled, the call has no effect. + /// + /// @param name the name of the fetch profile to be disabled + /// + /// @throws UnknownProfileException Indicates that the given name does not + /// match any known fetch profile names + /// + /// @see org.hibernate.annotations.FetchProfile void disableFetchProfile(String name) throws UnknownProfileException; - /** - * Obtain a {@linkplain LobHelper} for instances of {@link java.sql.Blob} - * and {@link java.sql.Clob}. - * - * @return an instance of {@link LobHelper} - * - * @deprecated Use {@link Hibernate#getLobHelper()} instead. - */ + /// Obtain a {@linkplain LobHelper} for instances of [java.sql.Blob] + /// and [java.sql.Clob]. + /// + /// @return an instance of [LobHelper] + /// + /// @deprecated Use [#getLobHelper()] instead. @Deprecated(since="7.0", forRemoval = true) LobHelper getLobHelper(); - /** - * Obtain the collection of all managed entities which belong to this - * persistence context. - * - * @since 7.0 - */ + /// Obtain the collection of all managed entities which belong to this + /// persistence context. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(); - /** - * Obtain a collection of all managed instances of the entity type with the - * given entity name which belong to this persistence context. - * - * @since 7.0 - */ + /// Obtain a collection of all managed instances of the entity type with the + /// given entity name which belong to this persistence context. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(String entityName); - /** - * Obtain a collection of all managed entities of the given type which belong - * to this persistence context. This operation is not polymorphic, and does - * not return instances of subtypes of the given entity type. - * - * @since 7.0 - */ + /// Obtain a collection of all managed entities of the given type which belong + /// to this persistence context. This operation is not polymorphic, and does + /// not return instances of subtypes of the given entity type. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(Class entityType); - /** - * Obtain a collection of all managed entities of the given type which belong - * to this persistence context. This operation is not polymorphic, and does - * not return instances of subtypes of the given entity type. - * - * @since 7.0 - */ + /// Obtain a collection of all managed entities of the given type which belong + /// to this persistence context. This operation is not polymorphic, and does + /// not return instances of subtypes of the given entity type. + /// + /// @since 7.0 @Incubating Collection getManagedEntities(EntityType entityType); - /** - * Add one or more listeners to the Session - * - * @param listeners the listener(s) to add - */ + /// Add one or more listeners to the Session + /// + /// @param listeners the listener(s) to add void addEventListeners(SessionEventListener... listeners); - /** - * Set a hint. The hints understood by Hibernate are enumerated by - * {@link org.hibernate.jpa.AvailableHints}. - * - * @see org.hibernate.jpa.HibernateHints - * @see org.hibernate.jpa.SpecHints - * - * @apiNote Hints are a - * {@linkplain jakarta.persistence.EntityManager#setProperty - * JPA-standard way} to control provider-specific behavior of the - * {@code EntityManager}. Clients of the native API defined by - * Hibernate should make use of type-safe operations of this - * interface. For example, {@link #enableFetchProfile(String)} - * should be used in preference to the hint - * {@link org.hibernate.jpa.HibernateHints#HINT_FETCH_PROFILE}. - */ + /// Set a hint. The hints understood by Hibernate are enumerated by + /// [org.hibernate.jpa.AvailableHints]. + /// + /// @see org.hibernate.jpa.HibernateHints + /// @see org.hibernate.jpa.SpecHints + /// + /// @apiNote Hints are a [JPA-standard way][jakarta.persistence.EntityManager#setProperty] + /// to control provider-specific behavior of the + /// [EntityManager]. Clients of the native API defined by + /// Hibernate should make use of type-safe operations of this + /// interface. For example, [#enableFetchProfile(String)] + /// should be used in preference to the hint [org.hibernate.jpa.HibernateHints#HINT_FETCH_PROFILE]. @Override void setProperty(String propertyName, Object value); - /** - * Create a new mutable instance of {@link EntityGraph}, with only - * a root node, allowing programmatic definition of the graph from - * scratch. - * - * @param rootType The root entity of the graph - * - * @see #find(EntityGraph, Object, FindOption...) - * @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) - * @see org.hibernate.graph.EntityGraphs#createGraph(jakarta.persistence.metamodel.EntityType) - */ + /// Create a new mutable instance of [EntityGraph], with only + /// a root node, allowing programmatic definition of the graph from + /// scratch. + /// + /// @param rootType The root entity of the graph + /// + /// @see #find(EntityGraph, Object, FindOption...) + /// @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) + /// @see org.hibernate.graph.EntityGraphs#createGraph(jakarta.persistence.metamodel.EntityType) @Override RootGraph createEntityGraph(Class rootType); - /** - * Create a new mutable instance of {@link EntityGraph}, based on - * a predefined {@linkplain jakarta.persistence.NamedEntityGraph - * named entity graph}, allowing customization of the graph, or - * return {@code null} if there is no predefined graph with the - * given name. - * - * @param graphName The name of the predefined named entity graph - * - * @apiNote This method returns {@code RootGraph}, requiring an - * unchecked typecast before use. It's cleaner to obtain a graph using - * {@link #createEntityGraph(Class, String)} instead. - * - * @see #find(EntityGraph, Object, FindOption...) - * @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) - * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) - */ + /// Create a new mutable instance of [EntityGraph], based on + /// a predefined [named entity graph][jakarta.persistence.NamedEntityGraph], + /// allowing customization of the graph, or return `null` if there is no + /// predefined graph with the given name. + /// + /// @param graphName The name of the predefined named entity graph + /// + /// @apiNote This method returns `RootGraph`, requiring an + /// unchecked typecast before use. It's cleaner to obtain a graph using + /// [#createEntityGraph(Class,String)] instead. + /// + /// @see #find(EntityGraph, Object, FindOption...) + /// @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) + /// @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) @Override RootGraph createEntityGraph(String graphName); - /** - * Obtain an immutable reference to a predefined - * {@linkplain jakarta.persistence.NamedEntityGraph named entity graph} - * or return {@code null} if there is no predefined graph with the given - * name. - * - * @param graphName The name of the predefined named entity graph - * - * @apiNote This method returns {@code RootGraph}, requiring an - * unchecked typecast before use. It's cleaner to obtain a graph using - * the static metamodel for the class which defines the graph, or by - * calling {@link SessionFactory#getNamedEntityGraphs(Class)} instead. - * - * @see #find(EntityGraph, Object, FindOption...) - * @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) - * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) - */ + /// Obtain an immutable reference to a predefined + /// [named entity graph][jakarta.persistence.NamedEntityGraph] + /// or return `null` if there is no predefined graph with the given + /// name. + /// + /// @param graphName The name of the predefined named entity graph + /// + /// @apiNote This method returns `RootGraph`, requiring an + /// unchecked typecast before use. It's cleaner to obtain a graph using + /// the static metamodel for the class which defines the graph, or by + /// calling [SessionFactory#getNamedEntityGraphs(Class)] instead. + /// + /// @see #find(EntityGraph, Object, FindOption...) + /// @see org.hibernate.query.SelectionQuery#setEntityGraph(EntityGraph, org.hibernate.graph.GraphSemantic) + /// @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) @Override RootGraph getEntityGraph(String graphName); - /** - * Retrieve all named {@link EntityGraph}s with the given root entity type. - * - * @see jakarta.persistence.EntityManagerFactory#getNamedEntityGraphs(Class) - * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) - */ + /// Retrieve all named [EntityGraph]s with the given root entity type. + /// + /// @see jakarta.persistence.EntityManagerFactory#getNamedEntityGraphs(Class) + /// @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) @Override List> getEntityGraphs(Class entityClass); @@ -1590,19 +1370,15 @@ public interface Session extends SharedSessionContract, EntityManager { @Override Query createQuery(CriteriaQuery criteriaQuery); - /** - * Create a {@link Query} for the given JPA {@link CriteriaDelete}. - * - * @deprecated use {@link #createMutationQuery(CriteriaDelete)} - */ + /// Create a [Query] for the given JPA [CriteriaDelete]. + /// + /// @deprecated use [#createMutationQuery(CriteriaDelete)] @Override @Deprecated(since = "6.0") @SuppressWarnings("rawtypes") Query createQuery(CriteriaDelete deleteQuery); - /** - * Create a {@link Query} for the given JPA {@link CriteriaUpdate}. - * - * @deprecated use {@link #createMutationQuery(CriteriaUpdate)} - */ + /// Create a [Query] for the given JPA [CriteriaUpdate]. + /// + /// @deprecated use [#createMutationQuery(CriteriaUpdate)] @Override @Deprecated(since = "6.0") @SuppressWarnings("rawtypes") Query createQuery(CriteriaUpdate updateQuery); diff --git a/hibernate-core/src/main/java/org/hibernate/Timeouts.java b/hibernate-core/src/main/java/org/hibernate/Timeouts.java index ddeae2940271..c96315975244 100644 --- a/hibernate-core/src/main/java/org/hibernate/Timeouts.java +++ b/hibernate-core/src/main/java/org/hibernate/Timeouts.java @@ -146,4 +146,17 @@ static int fromHint(Object factoryHint) { } return Integer.parseInt( factoryHint.toString() ); } + + static Timeout fromHintTimeout(Object factoryHint) { + if ( factoryHint == null ) { + return WAIT_FOREVER; + } + if ( factoryHint instanceof Timeout timeout ) { + return timeout; + } + if ( factoryHint instanceof Integer number ) { + return Timeout.milliseconds( number ); + } + return Timeout.milliseconds( Integer.parseInt( factoryHint.toString() ) ); + } } 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 530fe2441c2d..492ecd4462c8 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 @@ -906,26 +906,6 @@ public Object find(String entityName, Object primaryKey, FindOption... options) return delegate.find( entityName, primaryKey, options ); } - @Override - public T findByNaturalId(Class entityType, Object naturalId, FindOption... options) { - return delegate.findByNaturalId( entityType, naturalId, options ); - } - - @Override - public Object findByNaturalId(String entityName, Object naturalId, FindOption... options) { - return delegate.findByNaturalId( entityName, naturalId, options ); - } - - @Override - public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { - return delegate.findMultipleByNaturalId( entityType, naturalIds, options ); - } - - @Override - public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { - return delegate.findMultipleByNaturalId( entityName, naturalIds, options ); - } - @Override public T getReference(Class entityClass, Object id) { return delegate.getReference( entityClass, id ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java index 85857554d989..19fbf55a7a03 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java @@ -820,26 +820,6 @@ public Object find(String entityName, Object primaryKey, FindOption... options) return this.lazySession.get().find( entityName, primaryKey, options ); } - @Override - public T findByNaturalId(Class entityType, Object naturalId, FindOption... options) { - return lazySession.get().findByNaturalId( entityType, naturalId, options ); - } - - @Override - public Object findByNaturalId(String entityName, Object naturalId, FindOption... options) { - return lazySession.get().findByNaturalId( entityName, naturalId, options ); - } - - @Override - public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { - return lazySession.get().findMultipleByNaturalId( entityType, naturalIds, options ); - } - - @Override - public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { - return lazySession.get().findMultipleByNaturalId( entityName, naturalIds, options ); - } - @Override public void lock(Object entity, LockModeType lockMode) { this.lazySession.get().lock( entity, lockMode ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index f9de72ab4dac..00a7b5afcecc 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -28,7 +28,7 @@ /** * @author Steve Ebersole */ -class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess, MultiNaturalIdLoadOptions { +public class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess, MultiNaturalIdLoadOptions { private final EntityPersister entityDescriptor; private final SharedSessionContractImplementor session; @@ -42,7 +42,7 @@ class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess private RemovalsMode removalsMode = RemovalsMode.REPLACE; private OrderingMode orderingMode = OrderingMode.ORDERED; - NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SharedSessionContractImplementor session) { + public NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SharedSessionContractImplementor session) { this.entityDescriptor = entityDescriptor; this.session = session; } 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 b0a09b5bd558..321ff0a30f22 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -41,8 +41,9 @@ import org.hibernate.event.spi.*; import org.hibernate.event.spi.LoadEventListener.LoadType; import org.hibernate.graph.GraphSemantic; -import org.hibernate.graph.RootGraph; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.find.FindByKeyOperation; +import org.hibernate.internal.find.FindMultipleByKeyOperation; import org.hibernate.internal.util.ExceptionHelper; import org.hibernate.jpa.internal.LegacySpecHelper; import org.hibernate.jpa.internal.util.ConfigurationHelper; @@ -991,30 +992,45 @@ else if ( option instanceof RemovalsMode ) { } @Override - public List findMultiple(Class entityType, List ids, FindOption... options) { - final var loadAccess = byMultipleIds( entityType ); - final boolean isFindByNaturalId = setMultiIdentifierLoadAccessOptions( options, loadAccess ); - if ( isFindByNaturalId ) { - return findMultipleByNaturalId( entityType, ids, options ); - } - return loadAccess.multiLoad( ids ); + public List findMultiple(Class entityType, List keys, FindOption... options) { + //noinspection unchecked + return findMultiple( + requireEntityPersister( entityType ), + loadQueryInfluencers.getEffectiveEntityGraph().getSemantic(), + (RootGraphImplementor) loadQueryInfluencers.getEffectiveEntityGraph().getGraph(), + (List) keys, + options + ); + } + + private List findMultiple( + EntityPersister entityDescriptor, + GraphSemantic graphSemantic, + RootGraphImplementor rootGraph, + List keys, + FindOption... options) { + final var operation = new FindMultipleByKeyOperation( + entityDescriptor, + lockOptions, + getCacheMode(), + isDefaultReadOnly(), + getFactory(), + options + ); + return operation.performFind( keys, graphSemantic, rootGraph, this ); } @Override - public List findMultiple(EntityGraph entityGraph, List ids, FindOption... options) { - final var rootGraph = (RootGraph) entityGraph; + public List findMultiple(EntityGraph entityGraph, List keys, FindOption... options) { + final var rootGraph = (RootGraphImplementor) entityGraph; final var type = rootGraph.getGraphedType(); - final MultiIdentifierLoadAccess loadAccess = - switch ( type.getRepresentationMode() ) { - case MAP -> byMultipleIds( type.getTypeName() ); - case POJO -> byMultipleIds( type.getJavaType() ); - }; - loadAccess.withLoadGraph( rootGraph ); - final boolean isFindByNaturalId = setMultiIdentifierLoadAccessOptions( options, loadAccess ); - if ( isFindByNaturalId ) { - throw new UnsupportedOperationException( "Find by natural-id with entity-graph is not supported" ); - } - return loadAccess.multiLoad( ids ); + final var entityDescriptor = switch ( type.getRepresentationMode() ) { + case POJO -> requireEntityPersister( type.getJavaType() ); + case MAP -> requireEntityPersister( type.getTypeName() ); + }; + + //noinspection unchecked + return findMultiple( entityDescriptor, GraphSemantic.LOAD, rootGraph, (List) keys, options ); } @Override @@ -2331,28 +2347,22 @@ else if ( option instanceof FindMultipleOption findMultipleOption ) { @Override public T find(Class entityClass, Object key, FindOption... options) { - final IdentifierLoadAccessImpl loadAccess = byId( entityClass ); - final boolean isFindByNaturalId = setLoadAccessOptions( options, loadAccess ); - if ( isFindByNaturalId ) { - return findByNaturalId( entityClass, key, options ); - } - return loadAccess.load( key ); + //noinspection unchecked + return (T) byKey( requireEntityPersister( entityClass ), options ).performFind( key, this ); } @Override - public T find(EntityGraph entityGraph, Object primaryKey, FindOption... options) { - final var graph = (RootGraph) entityGraph; + public T find(EntityGraph entityGraph, Object key, FindOption... options) { + final var graph = (RootGraphImplementor) entityGraph; final var type = graph.getGraphedType(); - final IdentifierLoadAccessImpl loadAccess = - switch ( type.getRepresentationMode() ) { - case MAP -> byId( type.getTypeName() ); - case POJO -> byId( type.getJavaType() ); - }; - final boolean isFindByNaturalId = setLoadAccessOptions( options, loadAccess ); - if ( isFindByNaturalId ) { - throw new UnsupportedOperationException( "Find by natural-id with entity-graph is not supported" ); - } - return loadAccess.withLoadGraph( graph ).load( primaryKey ); + + final EntityPersister entityDescriptor = switch ( type.getRepresentationMode() ) { + case POJO -> requireEntityPersister( type.getJavaType() ); + case MAP -> requireEntityPersister( type.getTypeName() ); + }; + + //noinspection unchecked + return (T) byKey( entityDescriptor, GraphSemantic.LOAD, graph, options ).performFind( key, this ); } // Hibernate Reactive may need to use this @@ -2415,115 +2425,34 @@ private void checkTransactionNeededForUpdateOperation() { } @Override - public Object find(String entityName, Object primaryKey) { - final IdentifierLoadAccessImpl loadAccess = byId( entityName ); - return loadAccess.load( primaryKey ); - } - - @Override - public Object find(String entityName, Object primaryKey, FindOption... options) { - final IdentifierLoadAccessImpl loadAccess = byId( entityName ); - final boolean isFindByNaturalId = setLoadAccessOptions( options, loadAccess ); - if ( isFindByNaturalId ) { - return findByNaturalId( entityName, primaryKey, options ); - } - return loadAccess.load( primaryKey ); - } - - @Override - public T findByNaturalId(Class entityType, Object naturalId, FindOption... options) { - final SimpleNaturalIdLoadAccessImpl access = (SimpleNaturalIdLoadAccessImpl) bySimpleNaturalId( entityType ); - setOptions( options, access ); - return access.load( naturalId ); - } - - private void setOptions(FindOption[] options, SimpleNaturalIdLoadAccessImpl access) { - for ( FindOption option : options ) { - if ( option instanceof FindBy findBy ) { - if ( findBy == FindBy.ID ) { - throw new IllegalArgumentException( "Cannot use FindBy#ID with findByNaturalId" ); - } - } - else if ( option instanceof LockMode lockMode ) { - access.with( lockMode ); - } - else if ( option instanceof LockModeType lockModeType ) { - access.with( LockMode.fromJpaLockMode( lockModeType ) ); - } - else if ( option instanceof Locking.Scope scope ) { - access.with( scope ); - } - else if ( option instanceof PessimisticLockScope scope ) { - access.with( Locking.Scope.fromJpaScope( scope ) ); - } - else if ( option instanceof Timeout timeout ) { - access.with( timeout ); - } - else { - throw new IllegalArgumentException( "Illegal option: " + option ); - } - } - } - - @Override - public Object findByNaturalId(String entityName, Object naturalId, FindOption... options) { - final SimpleNaturalIdLoadAccessImpl access = (SimpleNaturalIdLoadAccessImpl) bySimpleNaturalId( entityName ); - setOptions( options, access ); - return access.load( naturalId ); + public Object find(String entityName, Object key) { + return byKey( requireEntityPersister( entityName ) ).performFind( key, this ); } @Override - public List findMultipleByNaturalId(Class entityType, List naturalIds, FindOption... options) { - final NaturalIdMultiLoadAccessStandard access = (NaturalIdMultiLoadAccessStandard) byMultipleNaturalId( entityType ); - setOptions( options, access ); - return access.multiLoad( naturalIds ); + public Object find(String entityName, Object key, FindOption... options) { + return byKey( requireEntityPersister( entityName ), options ).performFind( key, this ); } - private void setOptions(FindOption[] options, NaturalIdMultiLoadAccessStandard access) { - for ( FindOption option : options ) { - if ( option instanceof FindBy findBy ) { - if ( findBy == FindBy.ID ) { - throw new IllegalArgumentException( "Cannot use FindBy#ID with findMultipleByNaturalId" ); - } - } - else if ( option instanceof LockMode lockMode ) { - access.with( lockMode ); - } - else if ( option instanceof LockModeType lockModeType ) { - access.with( LockMode.fromJpaLockMode( lockModeType ) ); - } - else if ( option instanceof Locking.Scope scope ) { - access.with( scope ); - } - else if ( option instanceof PessimisticLockScope scope ) { - access.with( Locking.Scope.fromJpaScope( scope ) ); - } - else if ( option instanceof Timeout timeout ) { - access.with( timeout ); - } - else if ( option instanceof CacheMode cacheMode ) { - access.with( cacheMode ); - } - else if ( option instanceof BatchSize batchSize ) { - access.withBatchSize( batchSize.batchSize() ); - } - else if ( option instanceof RemovalsMode removalsMode ) { - access.with( removalsMode ); - } - else if ( option instanceof OrderingMode orderingMode ) { - access.with( orderingMode ); - } - else { - throw new IllegalArgumentException( "Illegal option: " + option ); - } - } + private FindByKeyOperation byKey(EntityPersister entityDescriptor, FindOption... options) { + return byKey( entityDescriptor, null, null, options ); } - @Override - public List findMultipleByNaturalId(String entityName, List naturalIds, FindOption... options) { - final NaturalIdMultiLoadAccessStandard access = (NaturalIdMultiLoadAccessStandard) byMultipleNaturalId( entityName ); - setOptions( options, access ); - return access.multiLoad( naturalIds ); + private FindByKeyOperation byKey( + EntityPersister entityDescriptor, + GraphSemantic graphSemantic, + RootGraphImplementor rootGraph, + FindOption... options) { + return new FindByKeyOperation<>( + entityDescriptor, + graphSemantic, + rootGraph, + lockOptions, + getCacheMode(), + isReadOnly(), + getFactory(), + options + ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java b/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java new file mode 100644 index 000000000000..fd71607edce8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java @@ -0,0 +1,335 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.find; + +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.FindOption; +import jakarta.persistence.LockModeType; +import jakarta.persistence.PessimisticLockScope; +import jakarta.persistence.Timeout; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.CacheMode; +import org.hibernate.EnabledFetchProfile; +import org.hibernate.FindBy; +import org.hibernate.FindMultipleOption; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.NaturalIdSynchronization; +import org.hibernate.ObjectNotFoundException; +import org.hibernate.ReadOnlyMode; +import org.hibernate.Timeouts; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.event.spi.LoadEventListener; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.jpa.internal.util.LockModeTypeHelper; +import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.loader.internal.LoadAccessContext; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.HibernateProxy; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +import static org.hibernate.Timeouts.WAIT_FOREVER; +import static org.hibernate.engine.spi.NaturalIdResolutions.INVALID_NATURAL_ID_REFERENCE; +import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; +import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; + +/** + * @author Steve Ebersole + */ +public class FindByKeyOperation implements NaturalIdLoader.Options { + private final EntityPersister entityDescriptor; + + private FindBy findBy = FindBy.ID; + + private CacheStoreMode cacheStoreMode; + private CacheRetrieveMode cacheRetrieveMode; + + private LockMode lockMode; + private Locking.Scope lockScope; + private Locking.FollowOn lockFollowOn; + private Timeout lockTimeout = WAIT_FOREVER; + + private ReadOnlyMode readOnlyMode; + + private RootGraphImplementor rootGraph; + private GraphSemantic graphSemantic; + + private Set enabledFetchProfiles; + + private NaturalIdSynchronization naturalIdSynchronization; + + public FindByKeyOperation( + @NonNull EntityPersister entityDescriptor, + @Nullable GraphSemantic graphSemantic, + @Nullable RootGraphImplementor rootGraph, + @Nullable LockOptions defaultLockOptions, + @Nullable CacheMode defaultCacheMode, + boolean defaultReadOnly, + @NonNull SessionFactoryImplementor sessionFactory, + FindOption... findOptions) { + this.entityDescriptor = entityDescriptor; + + this.graphSemantic = graphSemantic; + this.rootGraph = rootGraph; + + if ( defaultCacheMode != null ) { + cacheStoreMode = defaultCacheMode.getJpaStoreMode(); + cacheRetrieveMode = defaultCacheMode.getJpaRetrieveMode(); + } + + if ( defaultLockOptions != null ) { + lockMode = defaultLockOptions.getLockMode(); + lockScope = defaultLockOptions.getScope(); + lockTimeout = defaultLockOptions.getTimeout(); + lockFollowOn = defaultLockOptions.getFollowOnStrategy(); + } + if ( lockTimeout == WAIT_FOREVER ) { + final Object factoryTimeoutHint = sessionFactory.getProperties().get( HINT_SPEC_LOCK_TIMEOUT ); + if ( factoryTimeoutHint != null ) { + lockTimeout = Timeouts.fromHintTimeout( factoryTimeoutHint ); + } + } + + readOnlyMode = defaultReadOnly ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE; + + for ( FindOption option : findOptions ) { + if ( option instanceof FindBy findBy ) { + this.findBy = findBy; + } + else if ( option instanceof CacheStoreMode cacheStoreMode ) { + this.cacheStoreMode = cacheStoreMode; + } + else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { + this.cacheRetrieveMode = cacheRetrieveMode; + } + else if ( option instanceof CacheMode cacheMode ) { + this.cacheStoreMode = cacheMode.getJpaStoreMode(); + this.cacheRetrieveMode = cacheMode.getJpaRetrieveMode(); + } + else if ( option instanceof LockModeType lockModeType ) { + this.lockMode = LockModeTypeHelper.getLockMode( lockModeType ); + } + else if ( option instanceof LockMode lockMode ) { + this.lockMode = lockMode; + } + else if ( option instanceof Locking.Scope lockScope ) { + this.lockScope = lockScope; + } + else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { + this.lockScope = Locking.Scope.fromJpaScope( pessimisticLockScope ); + } + else if ( option instanceof Locking.FollowOn followOn ) { + this.lockFollowOn = followOn; + } + else if ( option instanceof Timeout timeout ) { + this.lockTimeout = timeout; + } + else if ( option instanceof ReadOnlyMode readOnlyMode) { + this.readOnlyMode = readOnlyMode; + } + else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { + this.enabledFetchProfile( enabledFetchProfile.profileName() ); + } + else if ( option instanceof NaturalIdSynchronization naturalIdSynchronization ) { + this.naturalIdSynchronization = naturalIdSynchronization; + } + else if ( option instanceof FindMultipleOption findMultipleOption ) { + throw new IllegalArgumentException( "Option '" + findMultipleOption + "' can only be used in 'findMultiple()'" ); + } + } + } + + private void enabledFetchProfile(String profileName) { + if ( enabledFetchProfiles == null ) { + enabledFetchProfiles = new HashSet<>(); + } + enabledFetchProfiles.add( profileName ); + } + + public T performFind(Object key, LoadAccessContext loadAccessContext) { + if ( findBy == FindBy.NATURAL_ID ) { + return findByNaturalId( key, loadAccessContext ); + } + else { + return findById( key, loadAccessContext ); + } + + } + + private T findByNaturalId(Object key, LoadAccessContext loadAccessContext) { + final SessionImplementor session = loadAccessContext.getSession(); + + performAnyNeededCrossReferenceSynchronizations( + naturalIdSynchronization == NaturalIdSynchronization.ENABLED, + entityDescriptor, + session + ); + + final NaturalIdMapping naturalIdMapping = entityDescriptor.getNaturalIdMapping(); + final var normalizedKey = naturalIdMapping.normalizeInput( key ); + + final Object cachedResolution = getCachedNaturalIdResolution( normalizedKey, loadAccessContext ); + if ( cachedResolution == INVALID_NATURAL_ID_REFERENCE ) { + return null; + } + + if ( cachedResolution != null ) { + return findById( cachedResolution, loadAccessContext ); + } + + return withOptions( loadAccessContext, () -> { + @SuppressWarnings("unchecked") + final T loaded = (T) entityDescriptor.getNaturalIdLoader() + .load( normalizedKey, this, session ); + if ( loaded != null ) { + final var persistenceContext = session.getPersistenceContextInternal(); + final var lazyInitializer = HibernateProxy.extractLazyInitializer( loaded ); + final var entity = lazyInitializer != null ? lazyInitializer.getImplementation() : loaded; + final var entry = persistenceContext.getEntry( entity ); + assert entry != null; + if ( entry.getStatus() == Status.DELETED ) { + return null; + } + } + return loaded; + } ); + } + + private T withOptions(LoadAccessContext loadAccessContext, Supplier action) { + final var session = loadAccessContext.getSession(); + final var influencers = session.getLoadQueryInfluencers(); + final var fetchProfiles = influencers.adjustFetchProfiles( null, enabledFetchProfiles ); + final var effectiveEntityGraph = rootGraph == null + ? null + : influencers.applyEntityGraph( rootGraph, graphSemantic ); + + final var readOnly = session.isDefaultReadOnly(); + session.setDefaultReadOnly( readOnlyMode == ReadOnlyMode.READ_ONLY ); + + final var cacheMode = session.getCacheMode(); + session.setCacheMode( CacheMode.fromJpaModes( cacheRetrieveMode, cacheStoreMode ) ); + + try { + return action.get(); + } + finally { + loadAccessContext.delayedAfterCompletion(); + if ( effectiveEntityGraph != null ) { + effectiveEntityGraph.clear(); + } + influencers.setEnabledFetchProfileNames( fetchProfiles ); + session.setDefaultReadOnly( readOnly ); + session.setCacheMode( cacheMode ); + } + } + + private Object getCachedNaturalIdResolution( + Object normalizedNaturalIdValue, + LoadAccessContext loadAccessContext) { + loadAccessContext.checkOpenOrWaitingForAutoClose(); + loadAccessContext.pulseTransactionCoordinator(); + + return loadAccessContext + .getSession() + .getPersistenceContextInternal() + .getNaturalIdResolutions() + .findCachedIdByNaturalId( normalizedNaturalIdValue, entityDescriptor ); + } + + private T findById(Object key, LoadAccessContext loadAccessContext) { + return withOptions( loadAccessContext, () -> { + final var session = loadAccessContext.getSession(); + Object result; + try { + result = loadAccessContext.load( + LoadEventListener.GET, + coerceId( key, session.getFactory() ), + entityDescriptor.getEntityName(), + makeLockOptions(), + readOnlyMode == ReadOnlyMode.READ_ONLY + ); + } + catch (ObjectNotFoundException notFoundException) { + // if session cache contains proxy for nonexisting object + result = null; + } + initializeIfNecessary( result ); + //noinspection unchecked + return (T) result; + } ); + } + + private LockOptions makeLockOptions() { + return Helper.makeLockOptions( lockMode, lockScope, lockTimeout, lockFollowOn ); + } + + // Used by Hibernate Reactive + protected Object coerceId(Object id, SessionFactoryImplementor factory) { + if ( factory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() ) { + return id; + } + + try { + return entityDescriptor.getIdentifierMapping().getJavaType().coerce( id, factory::getTypeConfiguration ); + } + catch ( Exception e ) { + throw new IllegalArgumentException( "Argument '" + id + + "' could not be converted to the identifier type of entity '" + + entityDescriptor.getEntityName() + "'" + + " [" + e.getMessage() + "]", e ); + } + } + + private void initializeIfNecessary(Object result) { + if ( result != null ) { + final var lazyInitializer = extractLazyInitializer( result ); + if ( lazyInitializer != null ) { + if ( lazyInitializer.isUninitialized() ) { + lazyInitializer.initialize(); + } + } + else { + final var enhancementMetadata = entityDescriptor.getBytecodeEnhancementMetadata(); + if ( enhancementMetadata.isEnhancedForLazyLoading() + && enhancementMetadata.extractLazyInterceptor( result ) + instanceof EnhancementAsProxyLazinessInterceptor lazinessInterceptor ) { + lazinessInterceptor.forceInitialize( result, null ); + } + } + } + } + + @Override + public LockMode getLockMode() { + return lockMode; + } + + @Override + public Timeout getLockTimeout() { + return lockTimeout; + } + + @Override + public Locking.Scope getLockScope() { + return lockScope; + } + + @Override + public Locking.FollowOn getLockFollowOn() { + return lockFollowOn; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java b/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java new file mode 100644 index 000000000000..30616a283cd2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java @@ -0,0 +1,339 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.find; + +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.FindOption; +import jakarta.persistence.LockModeType; +import jakarta.persistence.PessimisticLockScope; +import jakarta.persistence.Timeout; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.BatchSize; +import org.hibernate.CacheMode; +import org.hibernate.EnabledFetchProfile; +import org.hibernate.FindBy; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.NaturalIdSynchronization; +import org.hibernate.OrderingMode; +import org.hibernate.ReadOnlyMode; +import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; +import org.hibernate.Timeouts; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.jpa.internal.util.LockModeTypeHelper; +import org.hibernate.loader.ast.spi.MultiIdLoadOptions; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.loader.internal.LoadAccessContext; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.persister.entity.EntityPersister; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import static org.hibernate.Timeouts.WAIT_FOREVER; +import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; +import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT; + +/** + * @author Steve Ebersole + */ +public class FindMultipleByKeyOperation + implements MultiIdLoadOptions, MultiNaturalIdLoadOptions { + private final EntityPersister entityDescriptor; + + private BatchSize batchSize; + private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; + private RemovalsMode removalsMode = RemovalsMode.REPLACE; + private OrderingMode orderingMode = OrderingMode.ORDERED; + + private FindBy findBy = FindBy.ID; + + private CacheStoreMode cacheStoreMode; + private CacheRetrieveMode cacheRetrieveMode; + + private LockMode lockMode; + private Locking.Scope lockScope; + private Locking.FollowOn lockFollowOn; + private Timeout lockTimeout = WAIT_FOREVER; + + private ReadOnlyMode readOnlyMode; + + private Set enabledFetchProfiles; + + private NaturalIdSynchronization naturalIdSynchronization; + + public FindMultipleByKeyOperation( + @NonNull EntityPersister entityDescriptor, + @Nullable LockOptions defaultLockOptions, + @Nullable CacheMode defaultCacheMode, + boolean defaultReadOnly, + @NonNull SessionFactoryImplementor sessionFactory, + FindOption... findOptions) { + this.entityDescriptor = entityDescriptor; + + if ( defaultCacheMode != null ) { + cacheStoreMode = defaultCacheMode.getJpaStoreMode(); + cacheRetrieveMode = defaultCacheMode.getJpaRetrieveMode(); + } + + if ( defaultLockOptions != null ) { + lockMode = defaultLockOptions.getLockMode(); + lockScope = defaultLockOptions.getScope(); + lockTimeout = defaultLockOptions.getTimeout(); + lockFollowOn = defaultLockOptions.getFollowOnStrategy(); + } + if ( lockTimeout == WAIT_FOREVER ) { + final Object factoryTimeoutHint = sessionFactory.getProperties().get( HINT_SPEC_LOCK_TIMEOUT ); + if ( factoryTimeoutHint != null ) { + lockTimeout = Timeouts.fromHintTimeout( factoryTimeoutHint ); + } + } + + readOnlyMode = defaultReadOnly ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE; + + for ( FindOption option : findOptions ) { + if ( option instanceof FindBy findBy ) { + this.findBy = findBy; + } + else if ( option instanceof BatchSize batchSize ) { + this.batchSize = batchSize; + } + else if ( option instanceof SessionCheckMode sessionCheckMode ) { + this.sessionCheckMode = sessionCheckMode; + } + else if ( option instanceof RemovalsMode removalsMode ) { + this.removalsMode = removalsMode; + } + else if ( option instanceof OrderingMode orderingMode ) { + this.orderingMode = orderingMode; + } + else if ( option instanceof CacheStoreMode cacheStoreMode ) { + this.cacheStoreMode = cacheStoreMode; + } + else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { + this.cacheRetrieveMode = cacheRetrieveMode; + } + else if ( option instanceof CacheMode cacheMode ) { + this.cacheStoreMode = cacheMode.getJpaStoreMode(); + this.cacheRetrieveMode = cacheMode.getJpaRetrieveMode(); + } + else if ( option instanceof LockModeType lockModeType ) { + this.lockMode = LockModeTypeHelper.getLockMode( lockModeType ); + } + else if ( option instanceof LockMode lockMode ) { + this.lockMode = lockMode; + } + else if ( option instanceof Locking.Scope lockScope ) { + this.lockScope = lockScope; + } + else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { + this.lockScope = Locking.Scope.fromJpaScope( pessimisticLockScope ); + } + else if ( option instanceof Locking.FollowOn followOn ) { + this.lockFollowOn = followOn; + } + else if ( option instanceof Timeout timeout ) { + this.lockTimeout = timeout; + } + else if ( option instanceof ReadOnlyMode readOnlyMode) { + this.readOnlyMode = readOnlyMode; + } + else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { + this.enabledFetchProfile( enabledFetchProfile.profileName() ); + } + else if ( option instanceof NaturalIdSynchronization naturalIdSynchronization ) { + this.naturalIdSynchronization = naturalIdSynchronization; + } + } + } + + private void enabledFetchProfile(String profileName) { + if ( enabledFetchProfiles == null ) { + enabledFetchProfiles = new HashSet<>(); + } + enabledFetchProfiles.add( profileName ); + } + + public List performFind( + List keys, + @Nullable GraphSemantic graphSemantic, + @Nullable RootGraphImplementor rootGraph, + LoadAccessContext loadAccessContext) { + // todo (natural-id-class) : these impls are temporary + // longer term, move the logic here as much of it can be shared + if ( findBy == FindBy.NATURAL_ID ) { + return findByNaturalIds( keys, graphSemantic, rootGraph, loadAccessContext ); + } + else { + return findByIds( keys, graphSemantic, rootGraph, loadAccessContext ); + } + } + + private List findByNaturalIds(List keys, GraphSemantic graphSemantic, RootGraphImplementor rootGraph, LoadAccessContext loadAccessContext) { + final SessionImplementor session = loadAccessContext.getSession(); + + performAnyNeededCrossReferenceSynchronizations( + naturalIdSynchronization == NaturalIdSynchronization.ENABLED, + entityDescriptor, + session + ); + + final NaturalIdMapping naturalIdMapping = entityDescriptor.getNaturalIdMapping(); + + return withOptions( loadAccessContext, graphSemantic, rootGraph, () -> { + // normalize the incoming natural-id values and get them in array form as needed + // by MultiNaturalIdLoader + final Object[] naturalIds = new Object[keys.size()]; + for ( int i = 0; i < keys.size(); i++ ) { + final Object key = keys.get( i ); + naturalIds[i] = naturalIdMapping.normalizeInput( key ); + } + + //noinspection unchecked + return (List)entityDescriptor.getMultiNaturalIdLoader() + .multiLoad( naturalIds, this, session ); + } ); + } + + private List withOptions( + LoadAccessContext loadAccessContext, + GraphSemantic graphSemantic, + RootGraphImplementor rootGraph, + Supplier> action) { + final var session = loadAccessContext.getSession(); + final var influencers = session.getLoadQueryInfluencers(); + final var fetchProfiles = influencers.adjustFetchProfiles( null, enabledFetchProfiles ); + final var effectiveEntityGraph = rootGraph == null + ? null + : influencers.applyEntityGraph( rootGraph, graphSemantic ); + + final var readOnly = session.isDefaultReadOnly(); + session.setDefaultReadOnly( readOnlyMode == ReadOnlyMode.READ_ONLY ); + + final var cacheMode = session.getCacheMode(); + session.setCacheMode( CacheMode.fromJpaModes( cacheRetrieveMode, cacheStoreMode ) ); + + try { + return action.get(); + } + finally { + loadAccessContext.delayedAfterCompletion(); + if ( effectiveEntityGraph != null ) { + effectiveEntityGraph.clear(); + } + influencers.setEnabledFetchProfileNames( fetchProfiles ); + session.setDefaultReadOnly( readOnly ); + session.setCacheMode( cacheMode ); + } + } + + private Object getCachedNaturalIdResolution( + Object normalizedNaturalIdValue, + LoadAccessContext loadAccessContext) { + loadAccessContext.checkOpenOrWaitingForAutoClose(); + loadAccessContext.pulseTransactionCoordinator(); + + return loadAccessContext + .getSession() + .getPersistenceContextInternal() + .getNaturalIdResolutions() + .findCachedIdByNaturalId( normalizedNaturalIdValue, entityDescriptor ); + } + + private List findByIds(List keys, GraphSemantic graphSemantic, RootGraphImplementor rootGraph, LoadAccessContext loadAccessContext) { + final Object[] ids = keys.toArray( new Object[0] ); + //noinspection unchecked + return withOptions( loadAccessContext, graphSemantic, rootGraph, + () -> (List) entityDescriptor.multiLoad( ids, loadAccessContext.getSession(), this ) ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // MultiIdLoadOptions & MultiNaturalIdLoadOptions + + @Override + public SessionCheckMode getSessionCheckMode() { + return sessionCheckMode; + } + + @Override + public boolean isSecondLevelCacheCheckingEnabled() { + return cacheRetrieveMode == CacheRetrieveMode.USE; + } + + @Override + public Boolean getReadOnly(SessionImplementor session) { + return readOnlyMode == null ? null : readOnlyMode == ReadOnlyMode.READ_ONLY; + } + + @Override + public RemovalsMode getRemovalsMode() { + return removalsMode; + } + + @Override + public OrderingMode getOrderingMode() { + return orderingMode; + } + + @Override + public LockOptions getLockOptions() { + return Helper.makeLockOptions( lockMode, lockScope, lockTimeout, lockFollowOn ); + } + + @Override + public Integer getBatchSize() { + return batchSize == null ? null : batchSize.batchSize(); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Temporarily defined full constructor in support of + /// [org.hibernate.MultiIdentifierLoadAccess] and [org.hibernate.MultiIdentifierLoadAccess]. + /// + /// @deprecated [org.hibernate.MultiIdentifierLoadAccess] and [org.hibernate.MultiIdentifierLoadAccess] + /// are both also deprecated. + @Deprecated + public FindMultipleByKeyOperation( + EntityPersister entityDescriptor, + BatchSize batchSize, + SessionCheckMode sessionCheckMode, + RemovalsMode removalsMode, + OrderingMode orderingMode, + FindBy findBy, + CacheStoreMode cacheStoreMode, + CacheRetrieveMode cacheRetrieveMode, + LockMode lockMode, + Locking.Scope lockScope, + Locking.FollowOn lockFollowOn, + Timeout lockTimeout, + ReadOnlyMode readOnlyMode, + Set enabledFetchProfiles, + NaturalIdSynchronization naturalIdSynchronization) { + this.entityDescriptor = entityDescriptor; + this.batchSize = batchSize; + this.sessionCheckMode = sessionCheckMode; + this.removalsMode = removalsMode; + this.orderingMode = orderingMode; + this.findBy = findBy; + this.cacheStoreMode = cacheStoreMode; + this.cacheRetrieveMode = cacheRetrieveMode; + this.lockMode = lockMode; + this.lockScope = lockScope; + this.lockFollowOn = lockFollowOn; + this.lockTimeout = lockTimeout; + this.readOnlyMode = readOnlyMode; + this.enabledFetchProfiles = enabledFetchProfiles; + this.naturalIdSynchronization = naturalIdSynchronization; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java b/hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java new file mode 100644 index 000000000000..7a43743efbf5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/Helper.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.internal.find; + +import jakarta.persistence.Timeout; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.Timeouts; + +/** + * @author Steve Ebersole + */ +public class Helper { + public static LockOptions makeLockOptions(LockMode lockMode, Locking.Scope lockScope, Timeout lockTimeout, Locking.FollowOn lockFollowOn) { + if ( lockMode == null || lockMode == LockMode.NONE ) { + return LockOptions.NONE; + } + if ( lockMode == LockMode.READ ) { + return LockOptions.READ; + } + + final var lockOptions = new LockOptions( lockMode ); + lockOptions.setScope( lockScope != null ? lockScope : Locking.Scope.ROOT_ONLY ); + lockOptions.setTimeout( lockTimeout != null ? lockTimeout : Timeouts.WAIT_FOREVER ); + lockOptions.setFollowOnStrategy( lockFollowOn != null ? lockFollowOn : Locking.FollowOn.ALLOW ); + return lockOptions; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java index 600074f7b0e3..a8bd64be868a 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractNaturalIdLoader.java @@ -5,7 +5,10 @@ package org.hibernate.loader.ast.internal; import org.hibernate.HibernateException; +import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.Locking; +import org.hibernate.Timeouts; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -86,13 +89,28 @@ public EntityMappingType getLoadable() { } @Override - public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSessionContractImplementor session) { - final var factory = session.getFactory(); + public T load(Object naturalIdToLoad, Options options, SharedSessionContractImplementor session) { + final var lockOptions = makeLockOptions( options ); + return load( naturalIdToLoad, lockOptions, session ); + } - final var lockOptions = - options.getLockOptions() == null - ? new LockOptions() - : options.getLockOptions(); + private LockOptions makeLockOptions(Options options) { + if ( options.getLockMode() == null || options.getLockMode() == LockMode.NONE ) { + return LockOptions.NONE; + } + if ( options.getLockMode() == LockMode.READ ) { + return LockOptions.READ; + } + + final var lockOptions = new LockOptions( options.getLockMode() ); + lockOptions.setScope( options.getLockScope() != null ? options.getLockScope() : Locking.Scope.ROOT_ONLY ); + lockOptions.setTimeout( options.getLockTimeout() != null ? options.getLockTimeout() : Timeouts.WAIT_FOREVER ); + lockOptions.setFollowOnStrategy( options.getLockFollowOn() != null ? options.getLockFollowOn() : Locking.FollowOn.ALLOW ); + return lockOptions; + } + + private T load(Object naturalIdValue, LockOptions lockOptions, SharedSessionContractImplementor session) { + final var factory = session.getFactory(); final var sqlSelect = LoaderSelectBuilder.createSelect( getLoadable(), @@ -128,6 +146,14 @@ public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSession ); } + @Override + public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSessionContractImplementor session) { + final var lockOptions = options.getLockOptions() == null + ? new LockOptions() + : options.getLockOptions(); + return load( naturalIdValue, lockOptions, session ); + } + /** * Apply restriction necessary to match the given natural-id value. * Should also apply any predicates to the predicate consumer and 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 9333140979de..388c4e52a0d4 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 @@ -4,30 +4,46 @@ */ package org.hibernate.loader.ast.spi; +import jakarta.persistence.Timeout; +import org.hibernate.LockMode; +import org.hibernate.Locking; import org.hibernate.engine.spi.SharedSessionContractImplementor; -/** - * Loader for {@link org.hibernate.annotations.NaturalId} handling - * - * @author Steve Ebersole - */ +/// Loader for [org.hibernate.annotations.NaturalId] +/// +/// @author Steve Ebersole public interface NaturalIdLoader extends EntityLoader, MultiKeyLoader { + interface Options { + LockMode getLockMode(); + Timeout getLockTimeout(); + Locking.Scope getLockScope(); + Locking.FollowOn getLockFollowOn(); + } - /** - * Perform the load of the entity by its natural-id - * - * @param naturalIdToLoad The natural-id to load. One of 2 forms accepted: - * * Single-value - valid for entities with a simple (single-valued) - * natural-id - * * Map - valid for any natural-id load. The map is each value keyed - * by the attribute name that the value corresponds to. Even though - * this form is allowed for simple natural-ids, the single value form - * should be used as it is more efficient - * @param options The options to apply to the load operation - * @param session The session into which the entity is being loaded - */ + /// Perform the load of the entity by its natural-id + /// + /// @param naturalIdToLoad The natural-id to load. One of 2 forms accepted: + /// * Single-value - valid for entities with a simple (single-valued) + /// natural-id + /// * Map - valid for any natural-id load. The map is each value keyed + /// by the attribute name that the value corresponds to. Even though + /// this form is allowed for simple natural-ids, the single value form + /// should be used as it is more efficient + /// @param options The options to apply to the load operation + /// @param session The session into which the entity is being loaded + /// + /// @deprecated (since 7.3) : use [#load(Object, Options, SharedSessionContractImplementor)] instead. + @Deprecated T load(Object naturalIdToLoad, NaturalIdLoadOptions options, SharedSessionContractImplementor session); + /// Perform the load of the entity by its natural-id + /// + /// @param naturalIdToLoad The [normalized][org.hibernate.metamodel.mapping.NaturalIdMapping#normalizeInput] + /// form of the natural-id. + /// @param options The options to apply to the load operation + /// @param session The session into which the entity is being loaded + T load(Object naturalIdToLoad, Options options, SharedSessionContractImplementor session); + /** * Resolve the id from natural-id value */ diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java index d4d85891c72a..06025cd56eb9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/SimpleNaturalIdClassTests.java @@ -63,7 +63,7 @@ void testBootModel(DomainModelScope modelScope) { @Test void findBySimpleSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - var result = session.findByNaturalId( TestEntity2.class, "steve" ); + var result = session.find( TestEntity2.class, "steve", FindBy.NATURAL_ID ); assertEquals( 1, result.id ); } ); } @@ -71,7 +71,14 @@ void findBySimpleSmokeTest(SessionFactoryScope factoryScope) { @Test void findMultipleBySimpleSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - var results = session.findMultipleByNaturalId( TestEntity2.class, List.of( "steve", "john" ) ); + var results = session.findMultiple( TestEntity2.class, List.of( "steve", "john" ), FindBy.NATURAL_ID ); + assertThat( results ).hasSize( 2 ); + assertEquals( 1, results.get( 0 ).id ); + assertNull( results.get( 1 ) ); + } ); + // baseline + factoryScope.inTransaction( (session) -> { + var results = session.findMultiple( TestEntity2.class, List.of( 1, 2 ), FindBy.ID ); assertThat( results ).hasSize( 2 ); assertEquals( 1, results.get( 0 ).id ); assertNull( results.get( 1 ) ); @@ -81,7 +88,7 @@ void findMultipleBySimpleSmokeTest(SessionFactoryScope factoryScope) { @Test void findByClassSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - var result = session.findByNaturalId( TestEntity.class, new Key("steve", "ebersole") ); + var result = session.find( TestEntity.class, new Key("steve", "ebersole"), FindBy.NATURAL_ID ); assertEquals( 1, result.id ); } ); } @@ -97,7 +104,7 @@ void findByClassFindBySmokeTest(SessionFactoryScope factoryScope) { @Test void findMultipleByClassSmokeTest(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - var results = session.findMultipleByNaturalId( TestEntity.class, List.of( new Key("steve", "ebersole") ) ); + var results = session.findMultiple( TestEntity.class, List.of( new Key("steve", "ebersole") ), FindBy.NATURAL_ID ); assertThat( results ).hasSize( 1 ); assertEquals( 1, results.get( 0 ).id ); } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java index 0307279eaf80..47fdb95a9eaf 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/idclass/ToOneTests.java @@ -59,9 +59,10 @@ void testWouldBeNiceBaseline(SessionFactoryScope factoryScope) { // However, this is not supported atm. // See comments in `CompoundNaturalIdMapping.ToOneAttributeMapperImpl#extractFrom`. factoryScope.inTransaction( (session) -> { - session.findByNaturalId( + session.find( Order.class, - Map.of( "customer", 1, "invoiceNumber", 1001 ) + Map.of( "customer", 1, "invoiceNumber", 1001 ), + FindBy.NATURAL_ID ); } ); } From 75128c20031e2b8a218b5079971a506a477d7d65 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Fri, 21 Nov 2025 15:05:57 -0700 Subject: [PATCH 5/5] HHH-16383 - NaturalIdClass --- .../MultiIdentifierLoadAccessImpl.java | 80 +++++------ .../NaturalIdMultiLoadAccessStandard.java | 100 +++++--------- .../org/hibernate/internal/SessionImpl.java | 127 ------------------ .../internal/find/FindByKeyOperation.java | 9 +- .../find/FindMultipleByKeyOperation.java | 52 ++++--- 5 files changed, 105 insertions(+), 263 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java index e2fdedb0f9d1..4fa85e48ecb1 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java @@ -7,11 +7,15 @@ import jakarta.persistence.EntityGraph; import jakarta.persistence.PessimisticLockScope; import jakarta.persistence.Timeout; +import org.hibernate.BatchSize; import org.hibernate.CacheMode; +import org.hibernate.FindBy; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.MultiIdentifierLoadAccess; +import org.hibernate.NaturalIdSynchronization; import org.hibernate.OrderingMode; +import org.hibernate.ReadOnlyMode; import org.hibernate.RemovalsMode; import org.hibernate.SessionCheckMode; import org.hibernate.UnknownProfileException; @@ -19,19 +23,23 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.find.FindMultipleByKeyOperation; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; +import org.hibernate.loader.internal.LoadAccessContext; import org.hibernate.persister.entity.EntityPersister; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Supplier; import static java.util.Collections.emptyList; -/** - * @author Steve Ebersole - */ +/// Implementation of MultiIdentifierLoadAccess. +/// +/// @author Steve Ebersole +/// +/// @deprecated Use [FindMultipleByKeyOperation] instead. +@Deprecated class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, MultiIdLoadOptions { private final SharedSessionContractImplementor session; private final EntityPersister entityPersister; @@ -43,7 +51,7 @@ class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, private RootGraphImplementor rootGraph; private GraphSemantic graphSemantic; - private Integer batchSize; + private BatchSize batchSize; private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; private RemovalsMode removalsMode = RemovalsMode.REPLACE; protected OrderingMode orderingMode = OrderingMode.ORDERED; @@ -107,12 +115,12 @@ public MultiIdentifierLoadAccess with(EntityGraph graph, GraphSemantic sem @Override public Integer getBatchSize() { - return batchSize; + return batchSize.batchSize(); } @Override public MultiIdentifierLoadAccess withBatchSize(int batchSize) { - this.batchSize = batchSize < 1 ? null : batchSize; + this.batchSize = batchSize < 1 ? null : new BatchSize( batchSize ); return this; } @@ -164,45 +172,25 @@ public Boolean getReadOnly(SessionImplementor session) { @Override @SuppressWarnings( "unchecked" ) public List multiLoad(K... ids) { - return perform( () -> (List) entityPersister.multiLoad( ids, session, this ) ); - } - - public List perform(Supplier> executor) { - final var 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; - } - } + return buildOperation().performFind( List.of( ids ), graphSemantic, rootGraph, (LoadAccessContext) session ); + } - try { - final var influencers = session.getLoadQueryInfluencers(); - final var fetchProfiles = - influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles ); - final var effectiveEntityGraph = - rootGraph == null - ? null - : influencers.applyEntityGraph( rootGraph, graphSemantic ); - try { - return executor.get(); - } - finally { - if ( effectiveEntityGraph != null ) { - effectiveEntityGraph.clear(); - } - influencers.setEnabledFetchProfileNames( fetchProfiles ); - } - } - finally { - if ( cacheModeChanged ) { - // change it back - session.setCacheMode( sessionCacheMode ); - } - } + private FindMultipleByKeyOperation buildOperation() { + return new FindMultipleByKeyOperation( + entityPersister, + FindBy.ID, + batchSize, + sessionCheckMode, + removalsMode, + orderingMode, + cacheMode, + lockOptions, + readOnly == Boolean.TRUE ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE, + enabledFetchProfiles, + disabledFetchProfiles, + // irrelevant for load-by-id + NaturalIdSynchronization.DISABLED + ); } @Override @@ -210,7 +198,7 @@ public List perform(Supplier> executor) { public List multiLoad(List ids) { return ids.isEmpty() ? emptyList() - : perform( () -> (List) entityPersister.multiLoad( ids.toArray(), session, this ) ); + : buildOperation().performFind( (List)ids, graphSemantic, rootGraph, (LoadAccessContext) session ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index 00a7b5afcecc..914aced0ee65 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -4,30 +4,37 @@ */ package org.hibernate.internal; -import java.util.List; - import jakarta.persistence.EntityGraph; - import jakarta.persistence.PessimisticLockScope; import jakarta.persistence.Timeout; +import org.hibernate.BatchSize; import org.hibernate.CacheMode; +import org.hibernate.FindBy; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.Locking; import org.hibernate.NaturalIdMultiLoadAccess; +import org.hibernate.NaturalIdSynchronization; import org.hibernate.OrderingMode; +import org.hibernate.ReadOnlyMode; import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.internal.find.FindMultipleByKeyOperation; import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions; +import org.hibernate.loader.internal.LoadAccessContext; import org.hibernate.persister.entity.EntityPersister; -import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; +import java.util.List; -/** - * @author Steve Ebersole - */ +/// Implementation of NaturalIdMultiLoadAccess. +/// +/// @deprecated Use [FindMultipleByKeyOperation] instead. +/// +/// @author Steve Ebersole +@Deprecated public class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAccess, MultiNaturalIdLoadOptions { private final EntityPersister entityDescriptor; private final SharedSessionContractImplementor session; @@ -121,68 +128,31 @@ public void with(OrderingMode orderingMode) { @Override @SuppressWarnings( "unchecked" ) public List multiLoad(Object... ids) { - performAnyNeededCrossReferenceSynchronizations( true, entityDescriptor, session ); - - 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 var loadQueryInfluencers = session.getLoadQueryInfluencers(); - - try { - final var effectiveEntityGraph = loadQueryInfluencers.getEffectiveEntityGraph(); - final var initialGraphSemantic = effectiveEntityGraph.getSemantic(); - final var 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 { - // normalize the incoming natural-id values - for ( int i = 0; i < ids.length; i++ ) { - ids[i] = entityDescriptor.getNaturalIdMapping().normalizeInput( ids[ i ] ); - } - - return (List) - entityDescriptor.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 ); - } - } - + return buildOperation() + .performFind( List.of( ids ), graphSemantic, rootGraph, (LoadAccessContext) session ); } @Override public List multiLoad(List ids) { - return multiLoad( ids.toArray( new Object[ 0 ] ) ); + return buildOperation() + .performFind( (List) ids, graphSemantic, rootGraph, (LoadAccessContext) session ); + } + + private FindMultipleByKeyOperation buildOperation() { + return new FindMultipleByKeyOperation( + entityDescriptor, + FindBy.NATURAL_ID, + batchSize == null ? null : new BatchSize( batchSize ), + SessionCheckMode.ENABLED, + removalsMode, + orderingMode, + cacheMode, + lockOptions, + session.isDefaultReadOnly() ? ReadOnlyMode.READ_ONLY : ReadOnlyMode.READ_WRITE, + null, + null, + NaturalIdSynchronization.ENABLED + ); } @Override 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 321ff0a30f22..1cf1c01425d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -929,68 +929,6 @@ public void load(Object object, Object id) { fireLoad( new LoadEvent( id, object, this, getReadOnlyFromLoadQueryInfluencers() ), LoadEventListener.RELOAD ); } - private boolean setMultiIdentifierLoadAccessOptions(FindOption[] options, MultiIdentifierLoadAccess loadAccess) { - CacheStoreMode storeMode = getCacheStoreMode(); - CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); - LockOptions lockOptions = copySessionLockOptions(); - int batchSize = -1; - for ( FindOption option : options ) { - if ( option instanceof FindBy findBy ) { - if ( findBy == FindBy.NATURAL_ID ) { - return true; - } - } - if ( option instanceof CacheStoreMode cacheStoreMode ) { - storeMode = cacheStoreMode; - } - else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { - retrieveMode = cacheRetrieveMode; - } - else if ( option instanceof CacheMode cacheMode ) { - storeMode = cacheMode.getJpaStoreMode(); - retrieveMode = cacheMode.getJpaRetrieveMode(); - } - else if ( option instanceof LockModeType lockModeType ) { - lockOptions.setLockMode( LockModeTypeHelper.getLockMode( lockModeType ) ); - } - else if ( option instanceof LockMode lockMode ) { - lockOptions.setLockMode( lockMode ); - } - else if ( option instanceof LockOptions lockOpts ) { - lockOptions = lockOpts; - } - else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { - lockOptions.setLockScope( pessimisticLockScope ); - } - else if ( option instanceof Timeout timeout ) { - lockOptions.setTimeOut( timeout.milliseconds() ); - } - else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { - loadAccess.enableFetchProfile( enabledFetchProfile.profileName() ); - } - else if ( option instanceof ReadOnlyMode ) { - loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY ); - } - else if ( option instanceof BatchSize batchSizeOption ) { - batchSize = batchSizeOption.batchSize(); - } - else if ( option instanceof SessionCheckMode ) { - loadAccess.enableSessionCheck( option == SessionCheckMode.ENABLED ); - } - else if ( option instanceof OrderingMode ) { - loadAccess.enableOrderedReturn( option == OrderingMode.ORDERED ); - } - else if ( option instanceof RemovalsMode ) { - loadAccess.enableReturnOfDeletedEntities( option == RemovalsMode.INCLUDE ); - } - } - loadAccess.with( lockOptions ) - .with( interpretCacheMode( storeMode, retrieveMode ) ) - .withBatchSize( batchSize ); - - return false; - } - @Override public List findMultiple(Class entityType, List keys, FindOption... options) { //noinspection unchecked @@ -2280,71 +2218,6 @@ protected static void logIgnoringEntityNotFound(Class entityClass, Object } } - /// @return `true` if [FindBy#NATURAL_ID] was found - private boolean setLoadAccessOptions(FindOption[] options, IdentifierLoadAccessImpl loadAccess) { - CacheStoreMode storeMode = getCacheStoreMode(); - CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); - LockOptions lockOptions = copySessionLockOptions(); - for ( FindOption option : options ) { - if ( option instanceof FindBy findBy ) { - if ( findBy == FindBy.NATURAL_ID ) { - return true; - } - } - else if ( option instanceof CacheStoreMode cacheStoreMode ) { - storeMode = cacheStoreMode; - } - else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) { - retrieveMode = cacheRetrieveMode; - } - else if ( option instanceof CacheMode cacheMode ) { - storeMode = cacheMode.getJpaStoreMode(); - retrieveMode = cacheMode.getJpaRetrieveMode(); - } - else if ( option instanceof LockModeType lockModeType ) { - lockOptions.setLockMode( LockModeTypeHelper.getLockMode( lockModeType ) ); - } - else if ( option instanceof LockMode lockMode ) { - lockOptions.setLockMode( lockMode ); - } - else if ( option instanceof LockOptions lockOpts ) { - lockOptions = lockOpts; - } - else if ( option instanceof Locking.Scope lockScope ) { - lockOptions.setScope( lockScope ); - } - else if ( option instanceof PessimisticLockScope pessimisticLockScope ) { - lockOptions.setScope( Locking.Scope.fromJpaScope( pessimisticLockScope ) ); - } - else if ( option instanceof Locking.FollowOn followOn ) { - lockOptions.setFollowOnStrategy( followOn ); - } - else if ( option instanceof Timeout timeout ) { - lockOptions.setTimeout( timeout ); - } - else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { - loadAccess.enableFetchProfile( enabledFetchProfile.profileName() ); - } - else if ( option instanceof ReadOnlyMode ) { - loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY ); - } - else if ( option instanceof FindMultipleOption findMultipleOption ) { - throw new IllegalArgumentException( "Option '" + findMultipleOption + "' can only be used in 'findMultiple()'" ); - } - } - if ( lockOptions.getLockMode().isPessimistic() ) { - if ( lockOptions.getTimeOut() == WAIT_FOREVER_MILLI ) { - final Object factoryHint = getFactory().getProperties().get( HINT_SPEC_LOCK_TIMEOUT ); - if ( factoryHint != null ) { - lockOptions.setTimeOut( Timeouts.fromHint( factoryHint ) ); - } - } - } - loadAccess.with( lockOptions ).with( interpretCacheMode( storeMode, retrieveMode ) ); - - return false; - } - @Override public T find(Class entityClass, Object key, FindOption... options) { //noinspection unchecked diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java b/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java index fd71607edce8..539b1a4a731b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java @@ -47,9 +47,12 @@ import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; -/** - * @author Steve Ebersole - */ +/// Support for loading a single entity by key (either [id][FindBy#ID] or [natural-id][FindBy#NATURAL_ID]). +/// +/// @see org.hibernate.Session#find +/// @see FindBy +/// +/// @author Steve Ebersole public class FindByKeyOperation implements NaturalIdLoader.Options { private final EntityPersister entityDescriptor; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java b/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java index 30616a283cd2..4e4f9f1b2dfd 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/find/FindMultipleByKeyOperation.java @@ -45,20 +45,22 @@ import static org.hibernate.internal.NaturalIdHelper.performAnyNeededCrossReferenceSynchronizations; import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT; -/** - * @author Steve Ebersole - */ -public class FindMultipleByKeyOperation - implements MultiIdLoadOptions, MultiNaturalIdLoadOptions { +/// Support for loading multiple entities (of a type) by key (either [id][FindBy#ID] or [natural-id][FindBy#NATURAL_ID]). +/// +/// @see org.hibernate.Session#findMultiple +/// @see FindBy +/// +/// @author Steve Ebersole +public class FindMultipleByKeyOperation implements MultiIdLoadOptions, MultiNaturalIdLoadOptions { private final EntityPersister entityDescriptor; + private FindBy findBy = FindBy.ID; + private BatchSize batchSize; private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; private RemovalsMode removalsMode = RemovalsMode.REPLACE; private OrderingMode orderingMode = OrderingMode.ORDERED; - private FindBy findBy = FindBy.ID; - private CacheStoreMode cacheStoreMode; private CacheRetrieveMode cacheRetrieveMode; @@ -70,9 +72,11 @@ public class FindMultipleByKeyOperation private ReadOnlyMode readOnlyMode; private Set enabledFetchProfiles; + private Set disabledFetchProfiles; private NaturalIdSynchronization naturalIdSynchronization; + @SuppressWarnings("PatternVariableHidesField") public FindMultipleByKeyOperation( @NonNull EntityPersister entityDescriptor, @Nullable LockOptions defaultLockOptions, @@ -213,7 +217,7 @@ private List withOptions( Supplier> action) { final var session = loadAccessContext.getSession(); final var influencers = session.getLoadQueryInfluencers(); - final var fetchProfiles = influencers.adjustFetchProfiles( null, enabledFetchProfiles ); + final var fetchProfiles = influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles ); final var effectiveEntityGraph = rootGraph == null ? null : influencers.applyEntityGraph( rootGraph, graphSemantic ); @@ -306,34 +310,38 @@ public Integer getBatchSize() { @Deprecated public FindMultipleByKeyOperation( EntityPersister entityDescriptor, + FindBy findBy, BatchSize batchSize, SessionCheckMode sessionCheckMode, RemovalsMode removalsMode, OrderingMode orderingMode, - FindBy findBy, - CacheStoreMode cacheStoreMode, - CacheRetrieveMode cacheRetrieveMode, - LockMode lockMode, - Locking.Scope lockScope, - Locking.FollowOn lockFollowOn, - Timeout lockTimeout, + CacheMode cacheMode, + LockOptions lockOptions, ReadOnlyMode readOnlyMode, Set enabledFetchProfiles, + Set disabledFetchProfiles, NaturalIdSynchronization naturalIdSynchronization) { + if ( cacheMode == null ) { + cacheMode = CacheMode.NORMAL; + } + if ( lockOptions == null ) { + lockOptions = LockOptions.NONE; + } this.entityDescriptor = entityDescriptor; + this.findBy = findBy; this.batchSize = batchSize; this.sessionCheckMode = sessionCheckMode; this.removalsMode = removalsMode; this.orderingMode = orderingMode; - this.findBy = findBy; - this.cacheStoreMode = cacheStoreMode; - this.cacheRetrieveMode = cacheRetrieveMode; - this.lockMode = lockMode; - this.lockScope = lockScope; - this.lockFollowOn = lockFollowOn; - this.lockTimeout = lockTimeout; + this.cacheStoreMode = cacheMode.getJpaStoreMode(); + this.cacheRetrieveMode = cacheMode.getJpaRetrieveMode(); + this.lockMode = lockOptions.getLockMode(); + this.lockScope = lockOptions.getScope(); + this.lockFollowOn = lockOptions.getFollowOnStrategy(); + this.lockTimeout = lockOptions.getTimeout(); this.readOnlyMode = readOnlyMode; this.enabledFetchProfiles = enabledFetchProfiles; + this.disabledFetchProfiles = disabledFetchProfiles; this.naturalIdSynchronization = naturalIdSynchronization; } }