HHH-15739 deprecate @LazyToOne and @LazyCollection

and add some docs and cleanups
This commit is contained in:
Gavin 2022-11-21 22:13:24 +01:00 committed by Gavin King
parent c8ffee43ef
commit 5b5721f64b
18 changed files with 916 additions and 213 deletions

View File

@ -990,19 +990,7 @@ See the <<chapters/domain/associations.adoc#associations-JoinFormula,`@JoinFormu
[[annotations-hibernate-lazycollection]]
==== `@LazyCollection`
The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyCollection.html[`@LazyCollection`] annotation is used to specify the lazy fetching behavior of a given collection.
The possible values are given by the `https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyCollectionOption.html[LazyCollectionOption]` enumeration:
`TRUE`:: Load it when the state is requested.
`FALSE`:: Eagerly load it.
`EXTRA`:: Prefer extra queries over full collection loading.
The `TRUE` and `FALSE` values are deprecated since you should be using the Jakarta Persistence {jpaJavadocUrlPrefix}FetchType.html[`FetchType`] attribute of the <<annotations-jpa-elementcollection>>, <<annotations-jpa-onetomany>>, or <<annotations-jpa-manytomany>> collection.
The `EXTRA` value has no equivalent in the Jakarta Persistence specification, and it's used to avoid loading the entire collection even when the collection is accessed for the first time.
Each element is fetched individually using a secondary query.
See the <<chapters/fetching/Fetching.adoc#fetching-LazyCollection, `@LazyCollection` mapping>> section for more info.
The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Index.html[[line-through]#`@LazyCollection`#] annotation is deprecated.
[[annotations-hibernate-lazygroup]]
==== `@LazyGroup`
@ -1019,15 +1007,7 @@ See the <<chapters/pc/BytecodeEnhancement.adoc#BytecodeEnhancement-lazy-loading-
[[annotations-hibernate-lazytoone]]
==== `@LazyToOne`
The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyToOne.html[`@LazyToOne`] annotation is used to specify the laziness options, represented by https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyToOneOption.html[`LazyToOneOption`], available for a `@OneToOne` or `@ManyToOne` association.
`LazyToOneOption` defines the following alternatives:
FALSE:: Eagerly load the association. This one is not needed since the Jakarta Persistence `FetchType.EAGER` offers the same behavior.
NO_PROXY:: This option will fetch the association lazily while returning real entity object.
PROXY:: This option will fetch the association lazily while returning a proxy instead.
See the <<chapters/domain/associations.adoc#associations-one-to-one-bidirectional-lazy,`@LazyToOne` mapping example>> section for more info.
The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Index.html[[line-through]#`@LazyToOne`#] annotation is deprecated.
[[annotations-hibernate-listindexbase]]
==== `@ListIndexBase`

View File

@ -236,8 +236,7 @@ The only way to figure out whether there is an associated record on the child si
Because this can lead to N+1 query issues, it's much more efficient to use unidirectional `@OneToOne` associations with the `@MapsId` annotation in place.
However, if you really need to use a bidirectional association and want to make sure that this is always going to be fetched lazily,
then you need to enable lazy state initialization bytecode enhancement and use the
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyToOne.html[`@LazyToOne`] annotation as well.
then you need to enable lazy state initialization bytecode enhancement.
[[associations-one-to-one-bidirectional-lazy-example]]
.Bidirectional `@OneToOne` lazy parent-side association

View File

@ -15,14 +15,10 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
/**
* @author Vlad Mihalcea
*/
@ -58,7 +54,6 @@ public class OneToOneBidirectionalLazyTest extends BaseEntityManagerFunctionalTe
orphanRemoval = true,
fetch = FetchType.LAZY
)
@LazyToOne(LazyToOneOption.NO_PROXY)
private PhoneDetails details;
//Getters and setters are omitted for brevity

View File

@ -47,6 +47,47 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttrib
* are intended for use by generic code that must materialize an "amputated" graph of
* Hibernate entities. (For example, a library which deserializes entities from JSON.)
* <p>
* Lazy fetching of a {@linkplain jakarta.persistence.OneToOne one to one} or
* {@linkplain jakarta.persistence.ManyToOne many to one} association requires special
* bytecode tricks. The tricks used depend on whether build-time bytecode enhancement
* is enabled.
* <p>
* When bytecode enhancement is <em>not</em> used, an unfetched lazy association* is
* represented by a <em>proxy object</em> which holds the identifier (foreign key) of
* the associated entity instance.
* <ul>
* <li>The identifier property of the proxy object is set when the proxy is instantiated.
* The program may obtain the entity identifier value of an unfetched proxy, without
* triggering lazy fetching, by calling the corresponding getter method.
* (It's even possible to set an association to reference an unfetched proxy.)
* <li>A delegate entity instance is lazily fetched when any other method of the proxy
* is called. Once fetched, the proxy delegates all method invocations to the
* delegate.
* <li>The proxy does not have the same concrete type as the proxied delegate, and so
* {@link #getClass(Object)} must be used in place of {@link Object#getClass()},
* and this method fetches the entity by side-effect.
* <li>For a polymorphic association, the concrete type of the associated entity is
* not known until the delegate is fetched from the database, and so
* {@link #unproxy(Object, Class)}} must be used to perform typecasts, and
* {@link #getClass(Object)} must be used instead of the Java {@code instanceof}
* operator.
* </ul>
* When bytecode enhancement <em>is</em> used, there is no such indirection, but the
* associated entity instance is initially in an unloaded state, with only its
* identifier field set.
* <ul>
* <li>The identifier field of an unloaded entity instance is set when the unloaded
* instance is instantiated. The program may obtain the identifier of an unloaded
* entity, without triggering lazy loading, by accessing the field containing the
* identifier.
* <li>The remaining non-lazy state of the entity instance is loaded lazily when any
* other field is accessed.
* <li>Typecasts, the Java {@code instanceof} operator, and {@link Object#getClass()}
* may be used as normal.
* </ul>
* As an exception to the above rules, <em>polymorphic</em> associations always work
* as if bytecode enhancement was not enabled.
*<p>
* Graphs of Hibernate entities obtained from a {@link Session} are usually in an
* amputated form, with associations and collections replaced by proxies and lazy
* collections. (That is, by instances of the internal types {@link HibernateProxy}

View File

@ -10,18 +10,33 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collection;
import java.util.Map;
/**
* Specify the laziness of a collection, either a
* {@link jakarta.persistence.OneToMany} or
* {@link jakarta.persistence.ManyToMany} association,
* or an {@link jakarta.persistence.ElementCollection}.
* <p>
* This is an alternative to specifying the JPA
* {@link jakarta.persistence.FetchType}.
* {@link jakarta.persistence.FetchType}. This annotation
* is used to enable {@linkplain LazyCollectionOption#EXTRA
* extra-lazy collection fetching}.
*
* @author Emmanuel Bernard
*
* @deprecated
* <ul>
* <li>Use the JPA-defined {@link jakarta.persistence.FetchType#EAGER}
* instead of {@code LazyCollection(FALSE)}.
* <li>Use static methods of {@link org.hibernate.Hibernate},
* for example {@link org.hibernate.Hibernate#size(Collection)},
* {@link org.hibernate.Hibernate#contains(Collection, Object)},
* or {@link org.hibernate.Hibernate#get(Map, Object)} instead
* of {@code LazyCollection(EXTRA)}.
* </ul>
*/
@Deprecated(since="6.2")
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LazyCollection {

View File

@ -6,22 +6,54 @@
*/
package org.hibernate.annotations;
import java.util.Collection;
import java.util.Map;
/**
* Lazy options available for a collection.
* Enumerates the options for lazy loading of a
* {@linkplain jakarta.persistence.ElementCollection collection},
* {@linkplain jakarta.persistence.ManyToOne many to one association},
* or {@linkplain jakarta.persistence.ManyToMany many to many association}.
*
* @author Emmanuel Bernard
*
* @see LazyCollection
*
* @deprecated
* <ul>
* <li>Use the JPA-defined {@link jakarta.persistence.FetchType#EAGER}
* instead of {@code LazyCollection(FALSE)}.
* <li>Use static methods of {@link org.hibernate.Hibernate},
* for example {@link org.hibernate.Hibernate#size(Collection)},
* {@link org.hibernate.Hibernate#contains(Collection, Object)},
* or {@link org.hibernate.Hibernate#get(Map, Object)} instead
* of {@code LazyCollection(EXTRA)}.
* </ul>
*/
@Deprecated
public enum LazyCollectionOption {
/**
* Eagerly load it.
* The collection is always loaded eagerly, and all its
* elements are available immediately. However, access to
* the collection is still mediated by an instance of
* {@link org.hibernate.collection.spi.PersistentCollection},
* which tracks modifications to the collection.
*/
FALSE,
/**
* Load it when the state is requested.
* The underlying Java collection is proxied by an instance of
* {@link org.hibernate.collection.spi.PersistentCollection}
* and lazily fetched when a method of the proxy is called.
* All elements of the collection are retrieved at once.
*/
TRUE,
/**
* Prefer extra queries over full collection loading.
* The underlying Java collection is proxied by an instance of
* {@link org.hibernate.collection.spi.PersistentCollection}
* and its state is fetched lazily from the database as needed,
* when methods of the proxy are called. When reasonable, the
* proxy will avoid fetching all elements of the collection
* at once.
*/
EXTRA
}

View File

@ -14,52 +14,16 @@ import java.lang.annotation.Target;
/**
* Specifies the machinery used to handle lazy fetching of
* the annotated {@link jakarta.persistence.OneToOne} or
* {@link jakarta.persistence.ManyToOne} association. This
* is an alternative to specifying only the JPA
* {@link jakarta.persistence.FetchType}. This annotation
* is occasionally useful, since there are observable
* differences in semantics between:
* <ul>
* <li>{@linkplain LazyToOneOption#FALSE eager fetching},
* <li>lazy fetching via interception of calls on a
* {@linkplain LazyToOneOption#PROXY proxy object},
* and
* <li>lazy fetching via interception of field access on
* the {@linkplain LazyToOneOption#NO_PROXY owning side}
* of the association.
* </ul>
* By default, an unfetched lazy association is represented
* by a <em>proxy object</em> which holds the identifier
* (foreign key) of the associated entity instance.
* It's possible to obtain the identifier from an unfetched
* proxy, without fetching the entity from the database, by
* calling the corresponding getter method. (It's even
* possible to set an association to reference an unfetched
* proxy.) Lazy fetching occurs when any other method of the
* proxy is called. Once fetched, the proxy delegates all
* method invocations to the fetched entity instance.
* For a polymorphic association, the concrete type of the
* entity instance represented by a proxy is unknown, and
* so {@link org.hibernate.Hibernate#getClass(Object)} must
* be used to obtain the concrete type, fetching the entity
* by side effect. Similarly, typecasts must be performed
* using {@link org.hibernate.Hibernate#unproxy(Object, Class)}.
* <p>
* With {@code LazyToOne(NO_PROXY)}, an associated entity
* instance begins in an unloaded state, with only its
* identifier field set. Thus, it's possible to obtain the
* identifier if an unloaded entity, without triggering
* lazy loading. Typecasts, {@code instanceof}, and
* {@link Object#getClass()} work as normal. But this
* option is only available when bytecode enhancement is
* used.
* <p>
* <strong>Currently, Hibernate does not support
* {@code LazyToOne(NO_PROXY)} for polymorphic associations,
* and instead falls back to using a proxy!</strong>
* {@link jakarta.persistence.ManyToOne} association.
* This is an alternative to specifying only the JPA
* {@link jakarta.persistence.FetchType}.
*
* @author Emmanuel Bernard
*
* @deprecated use JPA annotations to specify the
* {@link jakarta.persistence.FetchType}
*/
@Deprecated(since="6.2")
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LazyToOne {

View File

@ -15,14 +15,21 @@ package org.hibernate.annotations;
* @author Emmanuel Bernard
*
* @see LazyToOne
*
* @deprecated since {@link LazyToOne} is deprecated, use
* {@link jakarta.persistence.FetchType} instead
*/
@Deprecated(since="6.2")
public enum LazyToOneOption {
/**
* The association is always loaded eagerly. The identifier
* and concrete type of the associated entity instance,
* along with all the rest of its non-lazy fields, are always
* available immediately.
*
* @deprecated use {@link jakarta.persistence.FetchType#EAGER}
*/
@Deprecated
FALSE,
/**
* The association is proxied and a delegate entity instance
@ -38,7 +45,8 @@ public enum LazyToOneOption {
* <li>The proxy does not have the same concrete type as the
* proxied delegate, and so
* {@link org.hibernate.Hibernate#getClass(Object)}
* must be used in place of {@link Object#getClass()}.
* must be used in place of {@link Object#getClass()},
* and this method fetches the entity by side-effect.
* <li>For a polymorphic association, the concrete type of
* the proxied entity instance is not known until the
* delegate is fetched from the database, and so
@ -48,7 +56,10 @@ public enum LazyToOneOption {
* must be used instead of the Java {@code instanceof}
* operator.
* </ul>
*
* @deprecated use {@link jakarta.persistence.FetchType#LAZY}
*/
@Deprecated
PROXY,
/**
* The associated entity instance is initially in an unloaded
@ -65,9 +76,11 @@ public enum LazyToOneOption {
* <li>Bytecode enhancement is required. If the class is not
* enhanced, this option is equivalent to {@link #PROXY}.
* </ul>
* <strong>Currently, Hibernate does not support this setting
* for polymorphic associations, and instead falls back to
* {@link #PROXY}!</strong>
* Hibernate does not support this setting for polymorphic
* associations, and instead falls back to {@link #PROXY}.
*
* @deprecated this setting no longer has any useful effect
*/
@Deprecated
NO_PROXY
}

View File

@ -1014,11 +1014,7 @@ public class BinderHelper {
}
private static GenerationType interpretGenerationType(GeneratedValue generatedValueAnn) {
if ( generatedValueAnn.strategy() == null ) {
return GenerationType.AUTO;
}
return generatedValueAnn.strategy();
return generatedValueAnn.strategy() == null ? GenerationType.AUTO : generatedValueAnn.strategy();
}
public static boolean isEmptyAnnotationValue(String annotationString) {
@ -1265,7 +1261,14 @@ public class BinderHelper {
}
public static FetchMode getFetchMode(FetchType fetch) {
return fetch == FetchType.EAGER ? FetchMode.JOIN : FetchMode.SELECT;
switch ( fetch ) {
case EAGER:
return FetchMode.JOIN;
case LAZY:
return FetchMode.SELECT;
default:
throw new AssertionFailure("unknown fetch type: " + fetch);
}
}
private static CascadeType convertCascadeType(jakarta.persistence.CascadeType cascade) {

View File

@ -131,31 +131,31 @@ public class OneToOneSecondPass implements SecondPass {
return !isEmptyAnnotationValue( mappedBy ) ? ForeignKeyDirection.TO_PARENT : ForeignKeyDirection.FROM_PARENT;
}
private void bindOwned(Map<String, PersistentClass> persistentClasses, OneToOne value, Property property) {
value.setMappedByProperty( mappedBy );
final PersistentClass targetEntity = persistentClasses.get( value.getReferencedEntityName() );
private void bindOwned(Map<String, PersistentClass> persistentClasses, OneToOne oneToOne, Property property) {
oneToOne.setMappedByProperty( mappedBy );
final PersistentClass targetEntity = persistentClasses.get( oneToOne.getReferencedEntityName() );
if ( targetEntity == null ) {
throw new MappingException( "Association '" + getPath( propertyHolder, inferredData )
+ "' targets unknown entity type '" + value.getReferencedEntityName() + "'" );
+ "' targets unknown entity type '" + oneToOne.getReferencedEntityName() + "'" );
}
final Property targetProperty = targetProperty( value, targetEntity );
final Property targetProperty = targetProperty( oneToOne, targetEntity );
if ( targetProperty.getValue() instanceof OneToOne ) {
propertyHolder.addProperty( property, inferredData.getDeclaringClass() );
}
else if ( targetProperty.getValue() instanceof ManyToOne ) {
bindTargetManyToOne( persistentClasses, value, property, targetEntity, targetProperty );
bindTargetManyToOne( persistentClasses, oneToOne, property, targetEntity, targetProperty );
}
else {
throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData )
+ "' is 'mappedBy' a property named '" + mappedBy
+ "' of the target entity type '" + value.getReferencedEntityName()
+ "' of the target entity type '" + oneToOne.getReferencedEntityName()
+ "' which is not a '@OneToOne' or '@ManyToOne' association" );
}
}
private void bindTargetManyToOne(
Map<String, PersistentClass> persistentClasses,
OneToOne value,
OneToOne oneToOne,
Property property,
PersistentClass targetEntity,
Property targetProperty) {
@ -174,11 +174,11 @@ public class OneToOneSecondPass implements SecondPass {
final ManyToOne manyToOne = new ManyToOne( buildingContext, mappedByJoin.getTable() );
//FIXME use ignore not found here
manyToOne.setNotFoundAction( notFoundAction );
manyToOne.setCascadeDeleteEnabled( value.isCascadeDeleteEnabled() );
manyToOne.setFetchMode( value.getFetchMode() );
manyToOne.setLazy( value.isLazy() );
manyToOne.setReferencedEntityName( value.getReferencedEntityName() );
manyToOne.setUnwrapProxy( value.isUnwrapProxy() );
manyToOne.setCascadeDeleteEnabled( oneToOne.isCascadeDeleteEnabled() );
manyToOne.setFetchMode( oneToOne.getFetchMode() );
manyToOne.setLazy( oneToOne.isLazy() );
manyToOne.setReferencedEntityName( oneToOne.getReferencedEntityName() );
manyToOne.setUnwrapProxy( oneToOne.isUnwrapProxy() );
manyToOne.markAsLogicalOneToOne();
property.setValue( manyToOne );
for ( Column column: otherSideJoin.getKey().getColumns() ) {
@ -203,7 +203,7 @@ public class OneToOneSecondPass implements SecondPass {
propertyHolder.addProperty( property, inferredData.getDeclaringClass() );
}
value.setReferencedPropertyName( mappedBy );
oneToOne.setReferencedPropertyName( mappedBy );
// HHH-6813
// Foo: @Id long id, @OneToOne(mappedBy="foo") Bar bar
@ -212,16 +212,16 @@ public class OneToOneSecondPass implements SecondPass {
boolean referenceToPrimaryKey = mappedBy == null
|| targetEntityIdentifier instanceof Component
&& !( (Component) targetEntityIdentifier ).hasProperty( mappedBy );
value.setReferenceToPrimaryKey( referenceToPrimaryKey );
oneToOne.setReferenceToPrimaryKey( referenceToPrimaryKey );
final String propertyRef = value.getReferencedPropertyName();
final String propertyRef = oneToOne.getReferencedPropertyName();
if ( propertyRef != null ) {
buildingContext.getMetadataCollector()
.addUniquePropertyReference( value.getReferencedEntityName(), propertyRef );
.addUniquePropertyReference( oneToOne.getReferencedEntityName(), propertyRef );
}
}
private Property targetProperty(OneToOne value, PersistentClass targetEntity) {
private Property targetProperty(OneToOne oneToOne, PersistentClass targetEntity) {
try {
Property targetProperty = findPropertyByName( targetEntity, mappedBy );
if ( targetProperty != null ) {
@ -233,10 +233,10 @@ public class OneToOneSecondPass implements SecondPass {
}
throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData )
+ "' is 'mappedBy' a property named '" + mappedBy
+ "' which does not exist in the target entity type '" + value.getReferencedEntityName() + "'" );
+ "' which does not exist in the target entity type '" + oneToOne.getReferencedEntityName() + "'" );
}
private void bindUnowned(Map<String, PersistentClass> persistentClasses, OneToOne value, String propertyName, Property property) {
private void bindUnowned(Map<String, PersistentClass> persistentClasses, OneToOne oneToOne, String propertyName, Property property) {
// we need to check if the columns are in the right order
// if not, then we need to create a many to one and formula
// but actually, since entities linked by a one to one need
@ -245,7 +245,7 @@ public class OneToOneSecondPass implements SecondPass {
if ( rightOrder ) {
final ToOneFkSecondPass secondPass = new ToOneFkSecondPass(
value,
oneToOne,
joinColumns,
!optional, //cannot have nullable and unique on certain DBs
propertyHolder.getPersistentClass(),
@ -253,7 +253,7 @@ public class OneToOneSecondPass implements SecondPass {
buildingContext
);
secondPass.doSecondPass(persistentClasses);
//no column associated since its a one to one
//no column associated since it's a one to one
propertyHolder.addProperty( property, inferredData.getDeclaringClass() );
}
// else {

View File

@ -300,50 +300,66 @@ public class ToOneBinder {
XProperty property,
PropertyData inferredData,
PropertyHolder propertyHolder) {
final FetchType fetchType = getJpaFetchType( property );
handleLazy( toOne, property, inferredData, propertyHolder );
handleFetch( toOne, property );
}
final LazyToOne lazy = property.getAnnotation( LazyToOne.class );
final NotFound notFound = property.getAnnotation( NotFound.class );
if ( notFound != null ) {
private static void handleFetch(ToOne toOne, XProperty property) {
if ( property.isAnnotationPresent( Fetch.class ) ) {
// Hibernate @Fetch annotation takes precedence
handleHibernateFetchMode( toOne, property );
}
else {
toOne.setFetchMode( getFetchMode( getJpaFetchType( property ) ) );
}
}
private static void handleLazy(ToOne toOne, XProperty property, PropertyData inferredData, PropertyHolder propertyHolder) {
if ( property.isAnnotationPresent( NotFound.class ) ) {
toOne.setLazy( false );
toOne.setUnwrapProxy( true );
}
else if ( lazy != null ) {
boolean lazyFalse = lazy.value() == LazyToOneOption.FALSE;
if ( fetchType == FetchType.LAZY && lazyFalse ) {
throw new AnnotationException("Association '" + getPath( propertyHolder, inferredData )
+ "' is marked 'fetch=LAZY' and '@LazyToOne(FALSE)'");
}
toOne.setLazy( !lazyFalse );
toOne.setUnwrapProxy( lazy.value() == LazyToOneOption.NO_PROXY );
}
else {
toOne.setLazy( fetchType == FetchType.LAZY );
toOne.setUnwrapProxy( fetchType != FetchType.LAZY );
boolean eager = isEager( property, inferredData, propertyHolder );
toOne.setLazy( !eager );
toOne.setUnwrapProxy( eager );
toOne.setUnwrapProxyImplicit( true );
}
}
final Fetch fetch = property.getAnnotation( Fetch.class );
if ( fetch != null ) {
// Hibernate @Fetch annotation takes precedence
if ( fetch.value() == org.hibernate.annotations.FetchMode.JOIN ) {
private static void handleHibernateFetchMode(ToOne toOne, XProperty property) {
switch ( property.getAnnotation( Fetch.class ).value() ) {
case JOIN:
toOne.setFetchMode( FetchMode.JOIN );
toOne.setLazy( false );
toOne.setUnwrapProxy( false );
}
else if ( fetch.value() == org.hibernate.annotations.FetchMode.SELECT ) {
break;
case SELECT:
toOne.setFetchMode( FetchMode.SELECT );
}
else if ( fetch.value() == org.hibernate.annotations.FetchMode.SUBSELECT ) {
break;
case SUBSELECT:
throw new AnnotationException( "Association '" + property.getName()
+ "' is annotated '@Fetch(SUBSELECT)' but is not many-valued");
default:
throw new AssertionFailure("unknown fetch type");
}
}
private static boolean isEager(XProperty property, PropertyData inferredData, PropertyHolder propertyHolder) {
final FetchType fetchType = getJpaFetchType( property );
if ( property.isAnnotationPresent( LazyToOne.class ) ) {
// LazyToOne takes precedent
final LazyToOne lazy = property.getAnnotation( LazyToOne.class );
boolean eager = lazy.value() == LazyToOneOption.FALSE;
if ( eager && fetchType == FetchType.LAZY ) {
// conflicts with non-default setting
throw new AnnotationException("Association '" + getPath(propertyHolder, inferredData)
+ "' is marked 'fetch=LAZY' and '@LazyToOne(FALSE)'");
}
return eager;
}
else {
throw new AssertionFailure( "Unknown FetchMode: " + fetch.value() );
}
}
else {
toOne.setFetchMode( getFetchMode( fetchType ) );
return fetchType == FetchType.EAGER;
}
}
@ -441,7 +457,8 @@ public class ToOneBinder {
//column.getTable() => persistentClass.getTable()
final String propertyName = inferredData.getPropertyName();
LOG.tracev( "Fetching {0} with {1}", propertyName, fetchMode );
if ( isMapToPK( joinColumns, propertyHolder, trueOneToOne ) || !isEmptyAnnotationValue( mappedBy ) ) {
if ( isMapToPK( joinColumns, propertyHolder, trueOneToOne )
|| !isEmptyAnnotationValue( mappedBy ) ) {
//is a true one-to-one
//FIXME referencedColumnName ignored => ordering may fail.
final OneToOneSecondPass secondPass = new OneToOneSecondPass(
@ -491,7 +508,7 @@ public class ToOneBinder {
}
else {
//try to find a hidden true one to one (FK == PK columns)
KeyValue identifier = propertyHolder.getIdentifier();
final KeyValue identifier = propertyHolder.getIdentifier();
if ( identifier == null ) {
//this is a @OneToOne in an @EmbeddedId (the persistentClass.identifier is not set yet, it's being built)
//by definition the PK cannot refer to itself so it cannot map to itself
@ -562,23 +579,24 @@ public class ToOneBinder {
}
private static boolean noConstraint(ForeignKey joinColumns, MetadataBuildingContext context) {
return joinColumns != null
&& ( joinColumns.value() == ConstraintMode.NO_CONSTRAINT
|| joinColumns.value() == ConstraintMode.PROVIDER_DEFAULT
&& context.getBuildingOptions().isNoConstraintByDefault() );
if ( joinColumns == null ) {
return false;
}
else {
ConstraintMode mode = joinColumns.value();
return mode == ConstraintMode.NO_CONSTRAINT
|| mode == ConstraintMode.PROVIDER_DEFAULT && context.getBuildingOptions().isNoConstraintByDefault();
}
}
public static String getReferenceEntityName(PropertyData propertyData, XClass targetEntity, MetadataBuildingContext context) {
if ( AnnotationBinder.isDefault( targetEntity, context ) ) {
return propertyData.getClassOrElementName();
}
else {
return targetEntity.getName();
}
return AnnotationBinder.isDefault( targetEntity, context )
? propertyData.getClassOrElementName()
: targetEntity.getName();
}
public static String getReferenceEntityName(PropertyData propertyData, MetadataBuildingContext context) {
XClass targetEntity = getTargetEntity( propertyData, context );
final XClass targetEntity = getTargetEntity( propertyData, context );
return AnnotationBinder.isDefault( targetEntity, context )
? propertyData.getClassOrElementName()
: targetEntity.getName();
@ -590,13 +608,13 @@ public class ToOneBinder {
}
private static Class<?> getTargetEntityClass(XProperty property) {
final ManyToOne mTo = property.getAnnotation( ManyToOne.class );
if (mTo != null) {
return mTo.targetEntity();
final ManyToOne manyToOne = property.getAnnotation( ManyToOne.class );
if ( manyToOne != null ) {
return manyToOne.targetEntity();
}
final OneToOne oTo = property.getAnnotation( OneToOne.class );
if (oTo != null) {
return oTo.targetEntity();
final OneToOne oneToOne = property.getAnnotation( OneToOne.class );
if ( oneToOne != null ) {
return oneToOne.targetEntity();
}
throw new AssertionFailure("Unexpected discovery of a targetEntity: " + property.getName() );
}

View File

@ -95,6 +95,8 @@ public class ToOneFkSecondPass extends FkSecondPass {
public void doSecondPass(java.util.Map<String, PersistentClass> persistentClasses) throws MappingException {
if ( value instanceof ManyToOne ) {
//TODO: move this validation logic to a separate ManyToOnSecondPass
// for consistency with how this is handled for OneToOnes
final ManyToOne manyToOne = (ManyToOne) value;
final PersistentClass targetEntity = persistentClasses.get( manyToOne.getReferencedEntityName() );
if ( targetEntity == null ) {

View File

@ -150,6 +150,8 @@ import org.hibernate.usertype.UserCollectionType;
import org.jboss.logging.Logger;
import static jakarta.persistence.AccessType.PROPERTY;
import static jakarta.persistence.FetchType.EAGER;
import static jakarta.persistence.FetchType.LAZY;
import static org.hibernate.cfg.AnnotatedColumn.buildColumnFromAnnotation;
import static org.hibernate.cfg.AnnotatedColumn.buildColumnFromNoAnnotation;
import static org.hibernate.cfg.AnnotatedColumn.buildColumnsFromAnnotations;
@ -1432,44 +1434,53 @@ public abstract class CollectionBinder {
}
private void defineFetchingStrategy() {
final FetchType jpaFetchType = getJpaFetchType();
handleLazy( jpaFetchType );
handleFetch( jpaFetchType );
handleLazy();
handleFetch();
}
private void handleFetch(FetchType jpaFetchType) {
final Fetch fetch = property.getAnnotation( Fetch.class );
if ( fetch != null ) {
private void handleFetch() {
if ( property.isAnnotationPresent( Fetch.class ) ) {
// Hibernate @Fetch annotation takes precedence
if ( fetch.value() == org.hibernate.annotations.FetchMode.JOIN ) {
handleHibernateFetchMode();
}
else {
collection.setFetchMode( getFetchMode( getJpaFetchType() ) );
}
}
private void handleHibernateFetchMode() {
switch ( property.getAnnotation( Fetch.class ).value() ) {
case JOIN:
collection.setFetchMode( FetchMode.JOIN );
collection.setLazy( false );
}
else if ( fetch.value() == org.hibernate.annotations.FetchMode.SELECT ) {
break;
case SELECT:
collection.setFetchMode( FetchMode.SELECT );
}
else if ( fetch.value() == org.hibernate.annotations.FetchMode.SUBSELECT ) {
break;
case SUBSELECT:
collection.setFetchMode( FetchMode.SELECT );
collection.setSubselectLoadable( true );
collection.getOwner().setSubselectLoadableCollections( true );
}
else {
throw new AssertionFailure( "Unknown FetchMode: " + fetch.value() );
}
}
else {
collection.setFetchMode( getFetchMode(jpaFetchType) );
break;
default:
throw new AssertionFailure( "unknown fetch type");
}
}
private void handleLazy(FetchType jpaFetchType) {
private void handleLazy() {
final FetchType jpaFetchType = getJpaFetchType();
if ( property.isAnnotationPresent( LazyCollection.class ) ) {
final LazyCollection lazy = property.getAnnotation( LazyCollection.class );
if ( lazy != null ) {
collection.setLazy( lazy.value() != LazyCollectionOption.FALSE );
boolean eager = lazy.value() == LazyCollectionOption.FALSE;
if ( !eager && jpaFetchType == EAGER ) {
throw new AnnotationException("Collection '" + safeCollectionRole()
+ "' is marked 'fetch=EAGER' and '@LazyCollection(" + lazy.value() + ")'");
}
collection.setLazy( !eager );
collection.setExtraLazy( lazy.value() == LazyCollectionOption.EXTRA );
}
else {
collection.setLazy( jpaFetchType == FetchType.LAZY );
collection.setLazy( jpaFetchType == LAZY );
collection.setExtraLazy( false );
}
}
@ -1489,7 +1500,7 @@ public abstract class CollectionBinder {
return elementCollection.fetch();
}
else if ( manyToAny != null ) {
return FetchType.LAZY;
return LAZY;
}
else {
throw new AssertionFailure(
@ -1526,27 +1537,8 @@ public abstract class CollectionBinder {
* return true if it's a Fk, false if it's an association table
*/
protected boolean bindStarToManySecondPass(Map<String, PersistentClass> persistentClasses) {
final XClass elementType = getElementType();
final PersistentClass persistentClass = persistentClasses.get( elementType.getName() );
boolean reversePropertyInJoin = false;
if ( persistentClass != null && hasMappedBy() ) {
try {
reversePropertyInJoin =
0 != persistentClass.getJoinNumber( persistentClass.getRecursiveProperty( mappedBy ) );
}
catch (MappingException e) {
throw new AnnotationException( "Collection '" + safeCollectionRole()
+ "' is 'mappedBy' a property named '" + mappedBy
+ "' which does not exist in the target entity '" + elementType.getName() + "'" );
}
}
if ( persistentClass != null
&& !reversePropertyInJoin
&& oneToMany
&& !isExplicitAssociationTable
&& ( joinColumns.getJoinColumns().get(0).isImplicit() && hasMappedBy() //implicit @JoinColumn
|| !foreignJoinColumns.getJoinColumns().get(0).isImplicit() ) //this is an explicit @JoinColumn
) {
final PersistentClass persistentClass = persistentClasses.get( getElementType().getName() );
if ( noAssociationTable( persistentClass ) ) {
//this is a foreign key
bindOneToManySecondPass( persistentClasses );
return true;
@ -1558,6 +1550,39 @@ public abstract class CollectionBinder {
}
}
private boolean isReversePropertyInJoin(XClass elementType, PersistentClass persistentClass) {
if ( persistentClass != null && hasMappedBy() ) {
try {
return persistentClass.getJoinNumber( persistentClass.getRecursiveProperty( mappedBy ) ) != 0;
}
catch (MappingException e) {
throw new AnnotationException( "Collection '" + safeCollectionRole()
+ "' is 'mappedBy' a property named '" + mappedBy
+ "' which does not exist in the target entity '" + elementType.getName() + "'" );
}
}
else {
return false;
}
}
private boolean noAssociationTable(PersistentClass persistentClass) {
return persistentClass != null
&& !isReversePropertyInJoin( getElementType(), persistentClass )
&& oneToMany
&& !isExplicitAssociationTable
&& ( implicitJoinColumn() || explicitForeignJoinColumn() );
}
private boolean implicitJoinColumn() {
return joinColumns.getJoinColumns().get(0).isImplicit()
&& hasMappedBy(); //implicit @JoinColumn
}
private boolean explicitForeignJoinColumn() {
return !foreignJoinColumns.getJoinColumns().get(0).isImplicit(); //this is an explicit @JoinColumn
}
private boolean hasMappedBy() {
return isNotEmpty( mappedBy );
}
@ -2357,7 +2382,7 @@ public abstract class CollectionBinder {
inverseJoinColumns,
inferredData,
cascadeDeleteEnabled,
anyAnn.fetch() == FetchType.LAZY,
anyAnn.fetch() == LAZY,
Nullability.NO_CONSTRAINT,
propertyHolder,
new EntityBinder(),

View File

@ -27,9 +27,9 @@ public abstract class ToOne extends SimpleValue implements Fetchable, SortableVa
private String propertyName;
private boolean lazy = true;
private boolean sorted;
protected boolean unwrapProxy;
protected boolean isUnwrapProxyImplicit;
protected boolean referenceToPrimaryKey = true;
private boolean unwrapProxy;
private boolean unwrapProxyImplicit;
private boolean referenceToPrimaryKey = true;
protected ToOne(MetadataBuildingContext buildingContext, Table table) {
super( buildingContext, table );
@ -44,7 +44,7 @@ public abstract class ToOne extends SimpleValue implements Fetchable, SortableVa
this.lazy = original.lazy;
this.sorted = original.sorted;
this.unwrapProxy = original.unwrapProxy;
this.isUnwrapProxyImplicit = original.isUnwrapProxyImplicit;
this.unwrapProxyImplicit = original.unwrapProxyImplicit;
this.referenceToPrimaryKey = original.referenceToPrimaryKey;
}
@ -135,7 +135,7 @@ public abstract class ToOne extends SimpleValue implements Fetchable, SortableVa
}
public boolean isUnwrapProxyImplicit() {
return isUnwrapProxyImplicit;
return unwrapProxyImplicit;
}
/**
@ -143,7 +143,7 @@ public abstract class ToOne extends SimpleValue implements Fetchable, SortableVa
* for reference later
*/
public void setUnwrapProxyImplicit(boolean unwrapProxyImplicit) {
isUnwrapProxyImplicit = unwrapProxyImplicit;
this.unwrapProxyImplicit = unwrapProxyImplicit;
}
public boolean isReferenceToPrimaryKey() {

View File

@ -0,0 +1,273 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.mapping.lazytoone;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import org.hibernate.Hibernate;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.EnhancementOptions;
import org.hibernate.testing.jdbc.SQLStatementInterceptor;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.math.BigDecimal;
import static jakarta.persistence.FetchType.LAZY;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Baseline test for uni-directional to-one, using an explicit @LazyToOne(NO_PROXY)
*
* @author Steve Ebersole
*/
@RunWith( BytecodeEnhancerRunner.class)
@EnhancementOptions( lazyLoading = true )
public class LanyProxylessManyToOneTests extends BaseNonConfigCoreFunctionalTestCase {
private SQLStatementInterceptor sqlStatementInterceptor;
@Override
protected void applyMetadataSources(MetadataSources sources) {
super.applyMetadataSources( sources );
sources.addAnnotatedClass( Customer.class );
sources.addAnnotatedClass( Order.class );
}
@Override
protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
super.configureStandardServiceRegistryBuilder( ssrb );
sqlStatementInterceptor = new SQLStatementInterceptor( ssrb );
}
@Test public void testLazyManyToOne() {
inTransaction(
(session) -> {
final Order order = session.byId(Order.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(false) );
assertThat( order.customer, nullValue() );
Customer customer = order.getCustomer();
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(true) );
assertThat( order.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getId(), is(1) );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getName(), is("Acme Brick") );
assertThat( Hibernate.isInitialized(customer), is(true) );
}
);
inTransaction(
(session) -> {
final Order order = session.byId(Order.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(false) );
assertThat( order.customer, nullValue() );
Customer customer = order.getCustomer();
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(true) );
assertThat( order.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
Hibernate.initialize( customer );
assertThat( Hibernate.isInitialized(customer), is(true) );
assertThat( customer.id, is(1) );
assertThat( customer.name, is("Acme Brick") );
}
);
}
@Test
public void testOwnerIsProxy() {
final EntityPersister orderDescriptor = sessionFactory().getMappingMetamodel().getEntityDescriptor( Order.class );
final BytecodeEnhancementMetadata orderEnhancementMetadata = orderDescriptor.getBytecodeEnhancementMetadata();
assertThat( orderEnhancementMetadata.isEnhancedForLazyLoading(), is( true ) );
final EntityPersister customerDescriptor = sessionFactory().getMappingMetamodel().getEntityDescriptor( Customer.class );
final BytecodeEnhancementMetadata customerEnhancementMetadata = customerDescriptor.getBytecodeEnhancementMetadata();
assertThat( customerEnhancementMetadata.isEnhancedForLazyLoading(), is( true ) );
inTransaction(
(session) -> {
final Order order = session.byId( Order.class ).getReference( 1 );
// we should have just the uninitialized proxy of the owner - and
// therefore no SQL statements should have been executed
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 0 ) );
final BytecodeLazyAttributeInterceptor initialInterceptor = orderEnhancementMetadata.extractLazyInterceptor( order );
assertThat( initialInterceptor, instanceOf( EnhancementAsProxyLazinessInterceptor.class ) );
// access the id - should do nothing with db
order.getId();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 0 ) );
assertThat( initialInterceptor, sameInstance( orderEnhancementMetadata.extractLazyInterceptor( order ) ) );
// this should trigger loading the entity's base state
order.getAmount();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
final BytecodeLazyAttributeInterceptor interceptor = orderEnhancementMetadata.extractLazyInterceptor( order );
assertThat( initialInterceptor, not( sameInstance( interceptor ) ) );
assertThat( interceptor, instanceOf( LazyAttributeLoadingInterceptor.class ) );
final LazyAttributeLoadingInterceptor attrInterceptor = (LazyAttributeLoadingInterceptor) interceptor;
assertThat( attrInterceptor.hasAnyUninitializedAttributes(), is( false ) );
// should not trigger a load and the `customer` reference should be an uninitialized enhanced proxy
final Customer customer = order.getCustomer();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
final BytecodeLazyAttributeInterceptor initialCustomerInterceptor = customerEnhancementMetadata.extractLazyInterceptor( customer );
assertThat( initialCustomerInterceptor, instanceOf( EnhancementAsProxyLazinessInterceptor.class ) );
// just as above, accessing id should trigger no loads
customer.getId();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
assertThat( initialCustomerInterceptor, sameInstance( customerEnhancementMetadata.extractLazyInterceptor( customer ) ) );
customer.getName();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 2 ) );
assertThat( customerEnhancementMetadata.extractLazyInterceptor( customer ), instanceOf( LazyAttributeLoadingInterceptor.class ) );
}
);
}
@Test
@TestForIssue(jiraKey = "HHH-14659")
public void testQueryJoinFetch() {
Order order = fromTransaction( (session) -> {
final Order result = session.createQuery(
"select o from Order o join fetch o.customer",
Order.class )
.uniqueResult();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
return result;
} );
// The "join fetch" should have already initialized the property,
// so that the getter can safely be called outside of a session.
assertTrue( Hibernate.isPropertyInitialized( order, "customer" ) );
// The "join fetch" should have already initialized the associated entity.
Customer customer = order.getCustomer();
assertTrue( Hibernate.isInitialized( customer ) );
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
}
@Before
public void createTestData() {
inTransaction(
(session) -> {
final Customer customer = new Customer( 1, "Acme Brick" );
session.persist( customer );
final Order order = new Order( 1, customer, BigDecimal.ONE );
session.persist( order );
}
);
sqlStatementInterceptor.clear();
}
@After
public void dropTestData() {
inTransaction(
(session) -> {
session.createQuery( "delete Order" ).executeUpdate();
session.createQuery( "delete Customer" ).executeUpdate();
}
);
}
@Entity( name = "Customer" )
@Table( name = "customer" )
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;
}
public Integer getId() {
return id;
}
private void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity( name = "Order")
@Table( name = "`order`")
public static class Order {
@Id
private Integer id;
@ManyToOne( fetch = LAZY )
private Customer customer;
private BigDecimal amount;
public Order() {
}
public Order(Integer id, Customer customer, BigDecimal amount) {
this.id = id;
this.customer = customer;
this.amount = amount;
}
public Integer getId() {
return id;
}
private void setId(Integer id) {
this.id = id;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
}

View File

@ -20,7 +20,6 @@ import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInter
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.TestForIssue;
@ -37,6 +36,8 @@ import static jakarta.persistence.FetchType.LAZY;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hibernate.annotations.LazyToOneOption.NO_PROXY;
@ -65,6 +66,41 @@ public class ManyToOneExplicitOptionTests extends BaseNonConfigCoreFunctionalTes
sqlStatementInterceptor = new SQLStatementInterceptor( ssrb );
}
@Test public void testLazyManyToOne() {
inTransaction(
(session) -> {
final Order order = session.byId(Order.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(false) );
assertThat( order.customer, nullValue() );
Customer customer = order.getCustomer();
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(true) );
assertThat( order.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getId(), is(1) );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getName(), is("Acme Brick") );
assertThat( Hibernate.isInitialized(customer), is(true) );
}
);
inTransaction(
(session) -> {
final Order order = session.byId(Order.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(false) );
assertThat( order.customer, nullValue() );
Customer customer = order.getCustomer();
assertThat( Hibernate.isPropertyInitialized( order, "customer"), is(true) );
assertThat( order.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
Hibernate.initialize( customer );
assertThat( Hibernate.isInitialized(customer), is(true) );
assertThat( customer.id, is(1) );
assertThat( customer.name, is("Acme Brick") );
}
);
}
@Test
public void testOwnerIsProxy() {
final EntityPersister orderDescriptor = sessionFactory().getMappingMetamodel().getEntityDescriptor( Order.class );

View File

@ -0,0 +1,271 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.mapping.lazytoone.onetoone;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import org.hibernate.Hibernate;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.EnhancementOptions;
import org.hibernate.testing.jdbc.SQLStatementInterceptor;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static jakarta.persistence.FetchType.LAZY;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Same as OneToOneExplicitOptionTests but using @Proxyless
*/
@RunWith( BytecodeEnhancerRunner.class)
@EnhancementOptions( lazyLoading = true )
public class LazyProxylessOneToOneTests extends BaseNonConfigCoreFunctionalTestCase {
private SQLStatementInterceptor sqlStatementInterceptor;
@Override
protected void applyMetadataSources(MetadataSources sources) {
super.applyMetadataSources( sources );
sources.addAnnotatedClass( Customer.class );
sources.addAnnotatedClass( SupplementalInfo.class );
}
@Override
protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
super.configureStandardServiceRegistryBuilder( ssrb );
sqlStatementInterceptor = new SQLStatementInterceptor( ssrb );
}
@Test public void testLazyOneToOne() {
inTransaction(
(session) -> {
final SupplementalInfo supplementalInfo = session.byId(SupplementalInfo.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(false) );
assertThat( supplementalInfo.customer, nullValue() );
Customer customer = supplementalInfo.getCustomer();
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(true) );
assertThat( supplementalInfo.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getId(), is(1) );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getName(), is("Acme Brick") );
assertThat( Hibernate.isInitialized(customer), is(true) );
}
);
inTransaction(
(session) -> {
final SupplementalInfo supplementalInfo = session.byId(SupplementalInfo.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(false) );
assertThat( supplementalInfo.customer, nullValue() );
Customer customer = supplementalInfo.getCustomer();
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(true) );
assertThat( supplementalInfo.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
Hibernate.initialize( customer );
assertThat( Hibernate.isInitialized(customer), is(true) );
assertThat( customer.id, is(1) );
assertThat( customer.name, is("Acme Brick") );
}
);
}
@Test
public void testOwnerIsProxy() {
final EntityPersister supplementalInfoDescriptor = sessionFactory().getMappingMetamodel().getEntityDescriptor( SupplementalInfo.class );
final BytecodeEnhancementMetadata supplementalInfoEnhancementMetadata = supplementalInfoDescriptor.getBytecodeEnhancementMetadata();
assertThat( supplementalInfoEnhancementMetadata.isEnhancedForLazyLoading(), is( true ) );
final EntityPersister customerDescriptor = sessionFactory().getMappingMetamodel().getEntityDescriptor( Customer.class );
final BytecodeEnhancementMetadata customerEnhancementMetadata = customerDescriptor.getBytecodeEnhancementMetadata();
assertThat( customerEnhancementMetadata.isEnhancedForLazyLoading(), is( true ) );
inTransaction(
(session) -> {
final SupplementalInfo supplementalInfo = session.byId( SupplementalInfo.class ).getReference( 1 );
// we should have just the uninitialized SupplementalInfo proxy
// - therefore no SQL statements should have been executed
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 0 ) );
final BytecodeLazyAttributeInterceptor initialInterceptor = supplementalInfoEnhancementMetadata.extractLazyInterceptor( supplementalInfo );
assertThat( initialInterceptor, instanceOf( EnhancementAsProxyLazinessInterceptor.class ) );
// access the id - should do nothing with db
supplementalInfo.getId();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 0 ) );
assertThat( supplementalInfoEnhancementMetadata.extractLazyInterceptor( supplementalInfo ), sameInstance( initialInterceptor ) );
// this should trigger loading the entity's base state
supplementalInfo.getSomething();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
final BytecodeLazyAttributeInterceptor interceptor = supplementalInfoEnhancementMetadata.extractLazyInterceptor( supplementalInfo );
assertThat( initialInterceptor, not( sameInstance( interceptor ) ) );
assertThat( interceptor, instanceOf( LazyAttributeLoadingInterceptor.class ) );
final LazyAttributeLoadingInterceptor attrInterceptor = (LazyAttributeLoadingInterceptor) interceptor;
assertThat( attrInterceptor.hasAnyUninitializedAttributes(), is( false ) );
// should not trigger a load and the `customer` reference should be an uninitialized enhanced proxy
final Customer customer = supplementalInfo.getCustomer();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
final BytecodeLazyAttributeInterceptor initialCustomerInterceptor = customerEnhancementMetadata.extractLazyInterceptor( customer );
assertThat( initialCustomerInterceptor, instanceOf( EnhancementAsProxyLazinessInterceptor.class ) );
// just as above, accessing id should trigger no loads
customer.getId();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
assertThat( initialCustomerInterceptor, sameInstance( customerEnhancementMetadata.extractLazyInterceptor( customer ) ) );
customer.getName();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 2 ) );
assertThat( customerEnhancementMetadata.extractLazyInterceptor( customer ), instanceOf( LazyAttributeLoadingInterceptor.class ) );
}
);
}
@Test
@TestForIssue(jiraKey = "HHH-14659")
public void testQueryJoinFetch() {
SupplementalInfo info = fromTransaction( (session) -> {
final SupplementalInfo result = session.createQuery(
"select s from SupplementalInfo s join fetch s.customer",
SupplementalInfo.class )
.uniqueResult();
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
return result;
} );
// The "join fetch" should have already initialized the property,
// so that the getter can safely be called outside a session.
assertTrue( Hibernate.isPropertyInitialized( info, "customer" ) );
// The "join fetch" should have already initialized the associated entity.
Customer customer = info.getCustomer();
assertTrue( Hibernate.isInitialized( customer ) );
assertThat( sqlStatementInterceptor.getSqlQueries().size(), is( 1 ) );
}
@Before
public void createTestData() {
inTransaction(
(session) -> {
final Customer customer = new Customer( 1, "Acme Brick" );
session.persist( customer );
final SupplementalInfo supplementalInfo = new SupplementalInfo( 1, customer, "extra details" );
session.persist( supplementalInfo );
}
);
sqlStatementInterceptor.clear();
}
@After
public void dropTestData() {
inTransaction(
(session) -> {
session.createQuery( "delete SupplementalInfo" ).executeUpdate();
session.createQuery( "delete Customer" ).executeUpdate();
}
);
}
@Entity( name = "Customer" )
@Table( name = "customer" )
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;
}
public Integer getId() {
return id;
}
private void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity( name = "SupplementalInfo" )
@Table( name = "supplemental" )
public static class SupplementalInfo {
@Id
private Integer id;
@OneToOne( fetch = LAZY, optional = false )
private Customer customer;
private String something;
public SupplementalInfo() {
}
public SupplementalInfo(Integer id, Customer customer, String something) {
this.id = id;
this.customer = customer;
this.something = something;
}
public Integer getId() {
return id;
}
private void setId(Integer id) {
this.id = id;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public String getSomething() {
return something;
}
public void setSomething(String something) {
this.something = something;
}
}
}

View File

@ -19,7 +19,6 @@ import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInter
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.TestForIssue;
@ -36,6 +35,8 @@ import static jakarta.persistence.FetchType.LAZY;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hibernate.annotations.LazyToOneOption.NO_PROXY;
@ -62,6 +63,41 @@ public class OneToOneExplicitOptionTests extends BaseNonConfigCoreFunctionalTest
sqlStatementInterceptor = new SQLStatementInterceptor( ssrb );
}
@Test public void testLazyOneToOne() {
inTransaction(
(session) -> {
final SupplementalInfo supplementalInfo = session.byId(SupplementalInfo.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(false) );
assertThat( supplementalInfo.customer, nullValue() );
Customer customer = supplementalInfo.getCustomer();
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(true) );
assertThat( supplementalInfo.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getId(), is(1) );
assertThat( Hibernate.isInitialized(customer), is(false) );
assertThat( customer.getName(), is("Acme Brick") );
assertThat( Hibernate.isInitialized(customer), is(true) );
}
);
inTransaction(
(session) -> {
final SupplementalInfo supplementalInfo = session.byId(SupplementalInfo.class).getReference(1);
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(false) );
assertThat( supplementalInfo.customer, nullValue() );
Customer customer = supplementalInfo.getCustomer();
assertThat( Hibernate.isPropertyInitialized( supplementalInfo, "customer"), is(true) );
assertThat( supplementalInfo.customer, notNullValue() );
assertThat( customer, notNullValue() );
assertThat( Hibernate.isInitialized(customer), is(false) );
Hibernate.initialize( customer );
assertThat( Hibernate.isInitialized(customer), is(true) );
assertThat( customer.getId(), is(1) );
assertThat( customer.getName(), is("Acme Brick") );
}
);
}
@Test
public void testOwnerIsProxy() {
final EntityPersister supplementalInfoDescriptor = sessionFactory().getMappingMetamodel().getEntityDescriptor( SupplementalInfo.class );
@ -129,7 +165,7 @@ public class OneToOneExplicitOptionTests extends BaseNonConfigCoreFunctionalTest
} );
// The "join fetch" should have already initialized the property,
// so that the getter can safely be called outside of a session.
// so that the getter can safely be called outside a session.
assertTrue( Hibernate.isPropertyInitialized( info, "customer" ) );
// The "join fetch" should have already initialized the associated entity.
Customer customer = info.getCustomer();