Skip to content

Commit 9050b78

Browse files
committed
HHH-10055 - Lazy loading of collections in enhanced entity not working
(cherry picked from commit 9d68861)
1 parent 5a8fc3e commit 9050b78

File tree

9 files changed

+494
-33
lines changed

9 files changed

+494
-33
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.bytecode.enhance.spi.interceptor;
8+
9+
import java.util.Locale;
10+
11+
import org.hibernate.FlushMode;
12+
import org.hibernate.LazyInitializationException;
13+
import org.hibernate.Session;
14+
import org.hibernate.engine.spi.SessionFactoryImplementor;
15+
import org.hibernate.engine.spi.SessionImplementor;
16+
import org.hibernate.internal.SessionFactoryRegistry;
17+
18+
import org.jboss.logging.Logger;
19+
20+
/**
21+
* @author Steve Ebersole
22+
*/
23+
public class Helper {
24+
private static final Logger log = Logger.getLogger( Helper.class );
25+
26+
interface Consumer {
27+
SessionImplementor getLinkedSession();
28+
boolean allowLoadOutsideTransaction();
29+
String getSessionFactoryUuid();
30+
}
31+
32+
interface LazyInitializationWork<T> {
33+
T doWork(SessionImplementor session, boolean isTemporarySession);
34+
35+
// informational details
36+
String getEntityName();
37+
String getAttributeName();
38+
}
39+
40+
41+
private final Consumer consumer;
42+
43+
public Helper(Consumer consumer) {
44+
this.consumer = consumer;
45+
}
46+
47+
public <T> T performWork(LazyInitializationWork<T> lazyInitializationWork) {
48+
SessionImplementor session = consumer.getLinkedSession();
49+
50+
boolean isTempSession = false;
51+
boolean isJta = false;
52+
53+
// first figure out which Session to use
54+
if ( session == null ) {
55+
if ( consumer.allowLoadOutsideTransaction() ) {
56+
session = openTemporarySessionForLoading( lazyInitializationWork );
57+
isTempSession = true;
58+
}
59+
else {
60+
throwLazyInitializationException( Cause.NO_SESSION, lazyInitializationWork );
61+
}
62+
}
63+
else if ( !session.isOpen() ) {
64+
if ( consumer.allowLoadOutsideTransaction() ) {
65+
session = openTemporarySessionForLoading( lazyInitializationWork );
66+
isTempSession = true;
67+
}
68+
else {
69+
throwLazyInitializationException( Cause.CLOSED_SESSION, lazyInitializationWork );
70+
}
71+
}
72+
else if ( !session.isConnected() ) {
73+
if ( consumer.allowLoadOutsideTransaction() ) {
74+
session = openTemporarySessionForLoading( lazyInitializationWork );
75+
isTempSession = true;
76+
}
77+
else {
78+
throwLazyInitializationException( Cause.DISCONNECTED_SESSION, lazyInitializationWork );
79+
}
80+
}
81+
82+
// If we are using a temporary Session, begin a transaction if necessary
83+
if ( isTempSession ) {
84+
isJta = session.getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta();
85+
86+
if ( !isJta ) {
87+
// Explicitly handle the transactions only if we're not in
88+
// a JTA environment. A lazy loading temporary session can
89+
// be created even if a current session and transaction are
90+
// open (ex: session.clear() was used). We must prevent
91+
// multiple transactions.
92+
( (Session) session ).beginTransaction();
93+
}
94+
}
95+
96+
try {
97+
// do the actual work
98+
return lazyInitializationWork.doWork( session, isTempSession );
99+
}
100+
finally {
101+
if ( isTempSession ) {
102+
try {
103+
// Commit the JDBC transaction is we started one.
104+
if ( !isJta ) {
105+
( (Session) session ).getTransaction().commit();
106+
}
107+
}
108+
catch (Exception e) {
109+
log.warn(
110+
"Unable to commit JDBC transaction on temporary session used to load lazy " +
111+
"collection associated to no session"
112+
);
113+
}
114+
115+
// Close the just opened temp Session
116+
try {
117+
( (Session) session ).close();
118+
}
119+
catch (Exception e) {
120+
log.warn( "Unable to close temporary session used to load lazy collection associated to no session" );
121+
}
122+
}
123+
}
124+
}
125+
126+
enum Cause {
127+
NO_SESSION,
128+
CLOSED_SESSION,
129+
DISCONNECTED_SESSION,
130+
NO_SF_UUID
131+
}
132+
133+
private void throwLazyInitializationException(Cause cause, LazyInitializationWork work) {
134+
final String reason;
135+
switch ( cause ) {
136+
case NO_SESSION: {
137+
reason = "no session and settings disallow loading outside the Session";
138+
break;
139+
}
140+
case CLOSED_SESSION: {
141+
reason = "session is closed and settings disallow loading outside the Session";
142+
break;
143+
}
144+
case DISCONNECTED_SESSION: {
145+
reason = "session is disconnected and settings disallow loading outside the Session";
146+
break;
147+
}
148+
case NO_SF_UUID: {
149+
reason = "could not determine SessionFactory UUId to create temporary Session for loading";
150+
break;
151+
}
152+
default: {
153+
reason = "<should never get here>";
154+
}
155+
}
156+
157+
final String message = String.format(
158+
Locale.ROOT,
159+
"Unable to perform requested lazy initialization [%s.%s] - %s",
160+
work.getEntityName(),
161+
work.getAttributeName(),
162+
reason
163+
);
164+
165+
throw new LazyInitializationException( message );
166+
}
167+
168+
private SessionImplementor openTemporarySessionForLoading(LazyInitializationWork lazyInitializationWork) {
169+
if ( consumer.getSessionFactoryUuid() == null ) {
170+
throwLazyInitializationException( Cause.NO_SF_UUID, lazyInitializationWork );
171+
}
172+
173+
final SessionFactoryImplementor sf = (SessionFactoryImplementor)
174+
SessionFactoryRegistry.INSTANCE.getSessionFactory( consumer.getSessionFactoryUuid() );
175+
final SessionImplementor session = (SessionImplementor) sf.openSession();
176+
session.getPersistenceContext().setDefaultReadOnly( true );
177+
session.setFlushMode( FlushMode.MANUAL );
178+
return session;
179+
}
180+
}

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/LazyAttributeLoader.java

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,63 +7,131 @@
77

88
package org.hibernate.bytecode.enhance.spi.interceptor;
99

10+
import java.io.Serializable;
1011
import java.util.Collection;
1112
import java.util.Set;
13+
import javax.naming.NamingException;
1214

13-
import org.hibernate.LazyInitializationException;
15+
import org.hibernate.LockMode;
1416
import org.hibernate.bytecode.enhance.internal.tracker.SimpleFieldTracker;
1517
import org.hibernate.bytecode.enhance.spi.CollectionTracker;
18+
import org.hibernate.bytecode.enhance.spi.interceptor.Helper.Consumer;
19+
import org.hibernate.bytecode.enhance.spi.interceptor.Helper.LazyInitializationWork;
1620
import org.hibernate.bytecode.instrumentation.spi.LazyPropertyInitializer;
1721
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
1822
import org.hibernate.engine.spi.SelfDirtinessTracker;
1923
import org.hibernate.engine.spi.SessionImplementor;
24+
import org.hibernate.engine.spi.Status;
25+
import org.hibernate.persister.entity.EntityPersister;
26+
27+
import org.jboss.logging.Logger;
2028

2129
/**
2230
* Interceptor that loads attributes lazily
2331
*
2432
* @author Luis Barreiro
2533
*/
26-
public class LazyAttributeLoader implements PersistentAttributeInterceptor {
34+
public class LazyAttributeLoader implements PersistentAttributeInterceptor, Consumer {
35+
private static final Logger log = Logger.getLogger( LazyAttributeLoader.class );
2736

2837
private transient SessionImplementor session;
38+
2939
private final Set<String> lazyFields;
3040
private final String entityName;
3141

42+
private String sessionFactoryUuid;
43+
private boolean allowLoadOutsideTransaction;
44+
3245
private final SimpleFieldTracker initializedFields = new SimpleFieldTracker();
3346

3447
public LazyAttributeLoader(SessionImplementor session, Set<String> lazyFields, String entityName) {
3548
this.session = session;
3649
this.lazyFields = lazyFields;
3750
this.entityName = entityName;
38-
}
3951

40-
protected final Object intercept(Object target, String fieldName, Object value) {
41-
if ( !isAttributeLoaded( fieldName ) ) {
42-
if ( session == null ) {
43-
throw new LazyInitializationException( "entity with lazy properties is not associated with a session" );
52+
this.allowLoadOutsideTransaction = session.getFactory().getSessionFactoryOptions().isInitializeLazyStateOutsideTransactionsEnabled();
53+
if ( this.allowLoadOutsideTransaction ) {
54+
try {
55+
this.sessionFactoryUuid = (String) session.getFactory().getReference().get( "uuid" ).getContent();
4456
}
45-
else if ( !session.isOpen() || !session.isConnected() ) {
46-
throw new LazyInitializationException( "session is not connected" );
57+
catch (NamingException e) {
58+
log.debug( "Unable to determine SF UUID in preparation for `allowLoadOutsideTransaction`" );
4759
}
60+
}
61+
}
4862

49-
Object loadedValue = ( (LazyPropertyInitializer) session.getFactory()
50-
.getEntityPersister( entityName ) ).initializeLazyProperty(
51-
fieldName,
52-
target,
53-
session
54-
);
55-
56-
initializedFields.add( fieldName );
57-
takeCollectionSizeSnapshot( target, fieldName, loadedValue );
58-
return loadedValue;
63+
protected final Object intercept(Object target, String attributeName, Object value) {
64+
if ( !isAttributeLoaded( attributeName ) ) {
65+
return loadAttribute( target, attributeName );
5966
}
6067
return value;
6168
}
6269

70+
private Object loadAttribute(final Object target, final String attributeName) {
71+
return new Helper( this ).performWork(
72+
new LazyInitializationWork() {
73+
@Override
74+
public Object doWork(SessionImplementor session, boolean isTemporarySession) {
75+
final EntityPersister persister = session.getFactory().getEntityPersister( getEntityName() );
76+
77+
if ( isTemporarySession ) {
78+
final Serializable id = persister.getIdentifier( target, null );
79+
80+
// Add an entry for this entity in the PC of the temp Session
81+
// NOTE : a few arguments that would be nice to pass along here...
82+
// 1) loadedState if we know any
83+
final Object[] loadedState = null;
84+
// 2) does a row exist in the db for this entity?
85+
final boolean existsInDb = true;
86+
// NOTE2: the final boolean is 'lazyPropertiesAreUnfetched' which is another
87+
// place where a "single lazy fetch group" shows up
88+
session.getPersistenceContext().addEntity(
89+
target,
90+
Status.READ_ONLY,
91+
loadedState,
92+
session.generateEntityKey( id, persister ),
93+
persister.getVersion( target ),
94+
LockMode.NONE,
95+
existsInDb,
96+
persister,
97+
true,
98+
true
99+
);
100+
}
101+
102+
final LazyPropertyInitializer initializer = (LazyPropertyInitializer) persister;
103+
final Object loadedValue = initializer.initializeLazyProperty(
104+
attributeName,
105+
target,
106+
session
107+
);
108+
109+
initializedFields.add( attributeName );
110+
takeCollectionSizeSnapshot( target, attributeName, loadedValue );
111+
return loadedValue;
112+
}
113+
114+
@Override
115+
public String getEntityName() {
116+
return entityName;
117+
}
118+
119+
@Override
120+
public String getAttributeName() {
121+
return attributeName;
122+
}
123+
}
124+
);
125+
}
126+
63127
public final void setSession(SessionImplementor session) {
64128
this.session = session;
65129
}
66130

131+
public final void unsetSession() {
132+
this.session = null;
133+
}
134+
67135
public boolean isAttributeLoaded(String fieldName) {
68136
return lazyFields == null || !lazyFields.contains( fieldName ) || initializedFields.contains( fieldName );
69137
}
@@ -220,4 +288,19 @@ public Object writeObject(Object obj, String name, Object oldValue, Object newVa
220288
}
221289
return newValue;
222290
}
291+
292+
@Override
293+
public SessionImplementor getLinkedSession() {
294+
return session;
295+
}
296+
297+
@Override
298+
public boolean allowLoadOutsideTransaction() {
299+
return allowLoadOutsideTransaction;
300+
}
301+
302+
@Override
303+
public String getSessionFactoryUuid() {
304+
return sessionFactoryUuid;
305+
}
223306
}

hibernate-core/src/main/java/org/hibernate/collection/internal/AbstractPersistentCollection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ public final boolean unsetSession(SessionImplementor currentSession) {
598598

599599
protected void prepareForPossibleLoadingOutsideTransaction() {
600600
if ( session != null ) {
601-
allowLoadOutsideTransaction = session.getFactory().getSettings().isInitializeLazyStateOutsideTransactionsEnabled();
601+
allowLoadOutsideTransaction = session.getFactory().getSessionFactoryOptions().isInitializeLazyStateOutsideTransactionsEnabled();
602602

603603
if ( allowLoadOutsideTransaction && sessionFactoryUuid == null ) {
604604
try {

hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.hibernate.PersistentObjectException;
3232
import org.hibernate.TransientObjectException;
3333
import org.hibernate.action.spi.AfterTransactionCompletionProcess;
34+
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoader;
3435
import org.hibernate.cache.spi.access.NaturalIdRegionAccessStrategy;
3536
import org.hibernate.cache.spi.access.SoftLock;
3637
import org.hibernate.collection.spi.PersistentCollection;
@@ -46,6 +47,8 @@
4647
import org.hibernate.engine.spi.EntityUniqueKey;
4748
import org.hibernate.engine.spi.ManagedEntity;
4849
import org.hibernate.engine.spi.PersistenceContext;
50+
import org.hibernate.engine.spi.PersistentAttributeInterceptable;
51+
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
4952
import org.hibernate.engine.spi.SessionFactoryImplementor;
5053
import org.hibernate.engine.spi.SessionImplementor;
5154
import org.hibernate.engine.spi.Status;
@@ -218,9 +221,21 @@ public void clear() {
218221
}
219222
((HibernateProxy) o).getHibernateLazyInitializer().unsetSession();
220223
}
224+
225+
for ( Entry<Object, EntityEntry> objectEntityEntryEntry : entityEntryContext.reentrantSafeEntityEntries() ) {
226+
// todo : I dont think this need be reentrant safe
227+
if ( objectEntityEntryEntry.getKey() instanceof PersistentAttributeInterceptable ) {
228+
final PersistentAttributeInterceptor interceptor = ( (PersistentAttributeInterceptable) objectEntityEntryEntry.getKey() ).$$_hibernate_getInterceptor();
229+
if ( interceptor instanceof LazyAttributeLoader ) {
230+
( (LazyAttributeLoader) interceptor ).unsetSession();
231+
}
232+
}
233+
}
234+
221235
for ( Map.Entry<PersistentCollection, CollectionEntry> aCollectionEntryArray : IdentityMap.concurrentEntries( collectionEntries ) ) {
222236
aCollectionEntryArray.getKey().unsetSession( getSession() );
223237
}
238+
224239
arrayHolders.clear();
225240
entitiesByKey.clear();
226241
entitiesByUniqueKey.clear();

0 commit comments

Comments
 (0)