Skip to content

Commit 4448d42

Browse files
committed
HHH-14466 : StackOverflowError loading an entity with eager one-to-many if bidirectional and many-to-one side is the ID
(cherry picked from commit 2bacaab)
1 parent 72bcd97 commit 4448d42

File tree

2 files changed

+296
-3
lines changed

2 files changed

+296
-3
lines changed

hibernate-core/src/main/java/org/hibernate/loader/plan/build/internal/FetchStyleLoadPlanBuildingAssociationVisitationStrategy.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@
1616
import org.hibernate.internal.CoreLogging;
1717
import org.hibernate.loader.plan.spi.CollectionReturn;
1818
import org.hibernate.loader.plan.spi.EntityReturn;
19+
import org.hibernate.loader.plan.spi.FetchSource;
1920
import org.hibernate.loader.plan.spi.LoadPlan;
2021
import org.hibernate.loader.plan.spi.Return;
22+
import org.hibernate.persister.collection.CollectionPersister;
23+
import org.hibernate.persister.entity.EntityPersister;
2124
import org.hibernate.persister.walking.spi.AssociationAttributeDefinition;
2225
import org.hibernate.persister.walking.spi.EncapsulatedEntityIdentifierDefinition;
2326
import org.hibernate.persister.walking.spi.EntityIdentifierDefinition;
2427
import org.hibernate.persister.walking.spi.NonEncapsulatedEntityIdentifierDefinition;
2528
import org.hibernate.persister.walking.spi.WalkingException;
29+
import org.hibernate.type.CompositeType;
30+
import org.hibernate.type.EmbeddedComponentType;
31+
import org.hibernate.type.EntityType;
32+
import org.hibernate.type.Type;
2633

2734
import org.jboss.logging.Logger;
2835

@@ -190,9 +197,59 @@ protected FetchStrategy adjustJoinFetchIfNeeded(
190197
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
191198
}
192199

193-
if ( attributeDefinition.getType().isCollectionType() && isTooManyCollections() ) {
194-
// todo : have this revert to batch or subselect fetching once "sql gen redesign" is in place
195-
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
200+
final FetchSource currentSource = currentSource();
201+
final Type attributeType = attributeDefinition.getType();
202+
203+
204+
if ( attributeType.isCollectionType() ) {
205+
if ( isTooManyCollections() ) {
206+
// todo : have this revert to batch or subselect fetching once "sql gen redesign" is in place
207+
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
208+
}
209+
if ( currentSource.resolveEntityReference() != null ) {
210+
CollectionPersister collectionPersister =
211+
(CollectionPersister) attributeDefinition.getType().getAssociatedJoinable( sessionFactory() );
212+
// Check if this is an eager "mappedBy" (inverse) side of a bidirectional
213+
// one-to-many/many-to-one association, with the many-to-one side
214+
// being the associated entity's ID as in:
215+
//
216+
// @Entity
217+
// public class Foo {
218+
// ...
219+
// @OneToMany(mappedBy = "foo", fetch = FetchType.EAGER)
220+
// private Set<Bar> bars = new HashSet<>();
221+
// }
222+
// @Entity
223+
// public class Bar implements Serializable {
224+
// @Id
225+
// @ManyToOne(fetch = FetchType.EAGER)
226+
// private Foo foo;
227+
// ...
228+
// }
229+
//
230+
if ( fetchStrategy.getTiming() == FetchTiming.IMMEDIATE &&
231+
fetchStrategy.getStyle() == FetchStyle.JOIN &&
232+
collectionPersister.isOneToMany() &&
233+
collectionPersister.isInverse() ) {
234+
// This is an eager "mappedBy" (inverse) side of a bidirectional
235+
// one-to-many/many-to-one association
236+
final EntityType elementType = (EntityType) collectionPersister.getElementType();
237+
final Type elementIdType = ( (EntityPersister) elementType.getAssociatedJoinable( sessionFactory() ) ).getIdentifierType();
238+
if ( elementIdType.isComponentType() && ( (CompositeType) elementIdType ).isEmbedded() ) {
239+
final EmbeddedComponentType elementIdTypeEmbedded = (EmbeddedComponentType) elementIdType;
240+
if ( elementIdTypeEmbedded.getSubtypes().length == 1 &&
241+
elementIdTypeEmbedded.getPropertyNames()[ 0 ].equals( collectionPersister.getMappedByProperty() ) ) {
242+
// The associated entity's ID is the other (many-to-one) side of the association.
243+
// The one-to-many side must be set to FetchMode.SELECT; otherwise,
244+
// there will be an infinite loop because the current entity
245+
// would need to be loaded before the associated entity can be loaded,
246+
// but the associated entity cannot be loaded until after the current
247+
// entity is loaded (since the current entity is the associated entity's ID).
248+
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
249+
}
250+
}
251+
}
252+
}
196253
}
197254

198255
return fetchStrategy;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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.test.annotations.derivedidentities.bidirectional;
8+
9+
import java.io.Serializable;
10+
import java.util.HashSet;
11+
import java.util.Objects;
12+
import java.util.Set;
13+
import javax.persistence.CascadeType;
14+
import javax.persistence.Entity;
15+
import javax.persistence.FetchType;
16+
import javax.persistence.Id;
17+
import javax.persistence.JoinColumn;
18+
import javax.persistence.ManyToOne;
19+
import javax.persistence.OneToMany;
20+
21+
import org.hibernate.Hibernate;
22+
23+
import org.hibernate.testing.TestForIssue;
24+
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
25+
import org.junit.After;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
29+
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
30+
import static org.junit.Assert.assertEquals;
31+
import static org.junit.Assert.assertNotNull;
32+
import static org.junit.Assert.assertSame;
33+
import static org.junit.Assert.assertTrue;
34+
35+
public class ManyToOneEagerDerivedIdFetchModeJoinTest extends BaseCoreFunctionalTestCase {
36+
private Foo foo;
37+
38+
@Test
39+
@TestForIssue(jiraKey = "HHH-14466")
40+
public void testQuery() {
41+
doInHibernate( this::sessionFactory, session -> {
42+
Bar newBar = (Bar) session.createQuery( "SELECT b FROM Bar b WHERE b.foo.id = :id" )
43+
.setParameter( "id", foo.getId() )
44+
.uniqueResult();
45+
assertNotNull( newBar );
46+
assertNotNull( newBar.getFoo() );
47+
assertTrue( Hibernate.isInitialized( newBar.getFoo() ) );
48+
assertEquals( foo.getId(), newBar.getFoo().getId() );
49+
assertTrue( Hibernate.isInitialized( newBar.getFoo().getBars() ) );
50+
assertEquals( 1, newBar.getFoo().getBars().size() );
51+
assertSame( newBar, newBar.getFoo().getBars().iterator().next() );
52+
assertEquals( "Some details", newBar.getDetails() );
53+
});
54+
}
55+
56+
@Test
57+
@TestForIssue(jiraKey = "HHH-14466")
58+
public void testQueryById() {
59+
60+
doInHibernate( this::sessionFactory, session -> {
61+
Bar newBar = (Bar) session.createQuery( "SELECT b FROM Bar b WHERE b.foo = :foo" )
62+
.setParameter( "foo", foo )
63+
.uniqueResult();
64+
assertNotNull( newBar );
65+
assertNotNull( newBar.getFoo() );
66+
assertTrue( Hibernate.isInitialized( newBar.getFoo() ) );
67+
assertEquals( foo.getId(), newBar.getFoo().getId() );
68+
assertTrue( Hibernate.isInitialized( newBar.getFoo().getBars() ) );
69+
assertEquals( 1, newBar.getFoo().getBars().size() );
70+
assertSame( newBar, newBar.getFoo().getBars().iterator().next() );
71+
assertEquals( "Some details", newBar.getDetails() );
72+
});
73+
}
74+
75+
@Test
76+
@TestForIssue(jiraKey = "HHH-14466")
77+
public void testFindByPrimaryKey() {
78+
79+
doInHibernate( this::sessionFactory, session -> {
80+
Bar newBar = session.find( Bar.class, foo.getId() );
81+
assertNotNull( newBar );
82+
assertNotNull( newBar.getFoo() );
83+
assertTrue( Hibernate.isInitialized( newBar.getFoo() ) );
84+
assertEquals( foo.getId(), newBar.getFoo().getId() );
85+
assertTrue( Hibernate.isInitialized( newBar.getFoo().getBars() ) );
86+
assertEquals( 1, newBar.getFoo().getBars().size() );
87+
assertSame( newBar, newBar.getFoo().getBars().iterator().next() );
88+
assertEquals( "Some details", newBar.getDetails() );
89+
});
90+
}
91+
92+
@Test
93+
@TestForIssue(jiraKey = "HHH-14466")
94+
public void testFindByInversePrimaryKey() {
95+
96+
doInHibernate( this::sessionFactory, session -> {
97+
Foo newFoo = session.find( Foo.class, foo.getId() );
98+
assertNotNull( newFoo );
99+
assertNotNull( newFoo.getBars() );
100+
assertTrue( Hibernate.isInitialized( newFoo.getBars() ) );
101+
assertEquals( 1, newFoo.getBars().size() );
102+
assertSame( newFoo, newFoo.getBars().iterator().next().getFoo() );
103+
assertEquals( "Some details", newFoo.getBars().iterator().next().getDetails() );
104+
});
105+
106+
}
107+
108+
@Before
109+
public void setupData() {
110+
this.foo = doInHibernate( this::sessionFactory, session -> {
111+
Foo foo = new Foo();
112+
foo.id = 1L;
113+
session.persist( foo );
114+
115+
Bar bar = new Bar();
116+
bar.setFoo( foo );
117+
bar.setDetails( "Some details" );
118+
119+
foo.getBars().add( bar );
120+
121+
session.persist( bar );
122+
123+
session.flush();
124+
125+
assertNotNull( foo.getId() );
126+
assertEquals( foo.getId(), bar.getFoo().getId() );
127+
128+
return foo;
129+
});
130+
}
131+
132+
@After
133+
public void cleanupData() {
134+
doInHibernate( this::sessionFactory, session -> {
135+
session.delete( session.find( Foo.class, foo.id ) );
136+
});
137+
this.foo = null;
138+
}
139+
140+
@Override
141+
protected Class<?>[] getAnnotatedClasses() {
142+
return new Class<?>[] {
143+
Foo.class,
144+
Bar.class,
145+
};
146+
}
147+
148+
@Entity(name = "Foo")
149+
public static class Foo {
150+
151+
@Id
152+
private Long id;
153+
154+
private String name;
155+
156+
@OneToMany(mappedBy = "foo", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
157+
private Set<Bar> bars = new HashSet<>();
158+
159+
public Long getId() {
160+
return id;
161+
}
162+
163+
public void setId(Long id) {
164+
this.id = id;
165+
}
166+
167+
public Set<Bar> getBars() {
168+
return bars;
169+
}
170+
171+
public void setBars(Set<Bar> bars) {
172+
this.bars = bars;
173+
}
174+
175+
@Override
176+
public boolean equals(Object o) {
177+
if ( this == o ) {
178+
return true;
179+
}
180+
if ( o == null || getClass() != o.getClass() ) {
181+
return false;
182+
}
183+
Foo foo = (Foo) o;
184+
return id.equals( foo.id );
185+
}
186+
187+
@Override
188+
public int hashCode() {
189+
return Objects.hash( id );
190+
}
191+
}
192+
193+
@Entity(name = "Bar")
194+
public static class Bar implements Serializable {
195+
196+
@Id
197+
@ManyToOne(fetch = FetchType.EAGER)
198+
@JoinColumn(name = "BAR_ID")
199+
private Foo foo;
200+
201+
private String details;
202+
203+
public Foo getFoo() {
204+
return foo;
205+
}
206+
207+
public void setFoo(Foo foo) {
208+
this.foo = foo;
209+
}
210+
211+
public String getDetails() {
212+
return details;
213+
}
214+
215+
public void setDetails(String details) {
216+
this.details = details;
217+
}
218+
219+
@Override
220+
public boolean equals(Object o) {
221+
if ( this == o ) {
222+
return true;
223+
}
224+
if ( o == null || getClass() != o.getClass() ) {
225+
return false;
226+
}
227+
Bar bar = (Bar) o;
228+
return foo.equals( bar.foo );
229+
}
230+
231+
@Override
232+
public int hashCode() {
233+
return Objects.hash( foo );
234+
}
235+
}
236+
}

0 commit comments

Comments
 (0)