Skip to content

Commit c0dcf76

Browse files
committed
HHH-19207 - create unit test to reproduce
1 parent 10a654c commit c0dcf76

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
package org.hibernate.orm.test.entitygraph;
2+
3+
import jakarta.persistence.CascadeType;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.FetchType;
6+
import jakarta.persistence.Id;
7+
import jakarta.persistence.JoinColumn;
8+
import jakarta.persistence.ManyToOne;
9+
import jakarta.persistence.NamedAttributeNode;
10+
import jakarta.persistence.NamedEntityGraph;
11+
import jakarta.persistence.NamedEntityGraphs;
12+
import jakarta.persistence.NamedSubgraph;
13+
import jakarta.persistence.OneToMany;
14+
import jakarta.persistence.OrderBy;
15+
import jakarta.persistence.criteria.JoinType;
16+
import jakarta.persistence.criteria.Predicate;
17+
import org.hibernate.Hibernate;
18+
import org.hibernate.graph.spi.RootGraphImplementor;
19+
import org.hibernate.query.Query;
20+
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
21+
import org.hibernate.query.criteria.JpaCriteriaQuery;
22+
import org.hibernate.query.criteria.JpaJoin;
23+
import org.hibernate.query.criteria.JpaRoot;
24+
import org.hibernate.testing.orm.junit.DomainModel;
25+
import org.hibernate.testing.orm.junit.Jira;
26+
import org.hibernate.testing.orm.junit.SessionFactory;
27+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
28+
import org.junit.jupiter.api.AfterAll;
29+
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Test;
31+
32+
import java.util.ArrayList;
33+
import java.util.Iterator;
34+
import java.util.LinkedHashSet;
35+
import java.util.List;
36+
import java.util.Random;
37+
import java.util.Set;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
41+
/**
42+
43+
*/
44+
@DomainModel(annotatedClasses = {
45+
FetchGraphCollectionOrderByAndCriteriaJoinTest.Level1.class,
46+
FetchGraphCollectionOrderByAndCriteriaJoinTest.Level2.class,
47+
FetchGraphCollectionOrderByAndCriteriaJoinTest.Level3.class,
48+
})
49+
@SessionFactory
50+
@Jira( "https://hibernate.atlassian.net/browse/HHH-19207" )
51+
public class FetchGraphCollectionOrderByAndCriteriaJoinTest {
52+
53+
@Test
54+
public void testJoinAndFilter(SessionFactoryScope scope) {
55+
executeTest( scope, true, true );
56+
}
57+
58+
@Test
59+
public void testNotJoinAndNotFilter(SessionFactoryScope scope) {
60+
executeTest( scope, false, false );
61+
}
62+
63+
/**
64+
* This case describes the problem of using a fetch graph with a collection that has an <code>@OrderBy</code> clause
65+
* and a criteria join without any usage.
66+
* <p>
67+
* This test case is expected to fail because the <code>@OrderBy</code> is not applied to the collection in the
68+
* generated SQL query.
69+
* <p>
70+
* The issue can also be solved by optimizing the criteria definition like in the test case
71+
* <code>testJoinAndFilter</code> or <code>testNotJoinAndNotFilter</code>, but there are some program code
72+
* structures where it is not possible to do it, or makes the source code more complex and less readable.
73+
* <p>
74+
* The required and the logical behaviour should be that the <code>@OrderBy</code> clause is applied to the
75+
* collection as in the other test cases. If this problem occurs very difficult to find out the reason because
76+
* this behaviour is not documented and the source code looks correct.
77+
*/
78+
@Test
79+
public void testJoinAndNotFilter(SessionFactoryScope scope) {
80+
executeTest( scope, true, false );
81+
}
82+
83+
84+
private void executeTest(SessionFactoryScope scope, boolean directJoin, boolean filterOnJoin) {
85+
scope.inTransaction( session -> {
86+
HibernateCriteriaBuilder builder = session.getCriteriaBuilder();
87+
JpaCriteriaQuery<Level1> criteriaQuery = builder.createQuery( Level1.class );
88+
JpaRoot<Level1> root = criteriaQuery.from( Level1.class );
89+
90+
List<Predicate> predicates = new ArrayList<>();
91+
predicates.add(
92+
builder.equal( root.get( "id" ), 1L )
93+
);
94+
95+
if ( directJoin || filterOnJoin ) {
96+
// Directly add the join to the level2 and level3 entities
97+
JpaJoin<Object, Object> join = root.join( "children", JoinType.INNER )
98+
.join( "children", JoinType.LEFT );
99+
100+
if ( filterOnJoin ) {
101+
predicates.add(
102+
builder.gt( join.get( "id" ), 1L )
103+
);
104+
}
105+
}
106+
107+
// Add all defined predicates to the criteria query
108+
criteriaQuery.where( builder.and( predicates ) );
109+
110+
// Set some default root ordering (not required for the test case)
111+
criteriaQuery.orderBy( builder.asc( root.get( "id" ) ) );
112+
113+
// Create the TypedQuery with entity graph
114+
RootGraphImplementor<?> graph = session.getEntityGraph( "level1_loadAll" );
115+
Query<Level1> query = session
116+
.createQuery( criteriaQuery )
117+
.setHint( org.hibernate.jpa.SpecHints.HINT_SPEC_FETCH_GRAPH, graph );
118+
119+
// Parse the result as stream, but the problem occurs also with getResultList()
120+
query.getResultStream().forEach( level1 -> {
121+
122+
// Check ordering of Level2 entities
123+
Long ordinalLevel2 = 0L;
124+
assertThat( level1.getChildren() ).matches( Hibernate::isInitialized );
125+
for ( Level2 level2 : level1.getChildren() ) {
126+
System.out.println( "Level2: " + level2.getOrdinal() );
127+
assertThat( level2.getOrdinal() ).isGreaterThan( ordinalLevel2 );
128+
ordinalLevel2 = level2.getOrdinal();
129+
130+
// Check ordering of Level3 entities
131+
Long ordinalLevel3 = 0L;
132+
assertThat( level2.getChildren() ).matches( Hibernate::isInitialized );
133+
for ( Level3 level3 : level2.getChildren() ) {
134+
System.out.println( "Level3: " + level3.getOrdinal() );
135+
assertThat( level3.getOrdinal() ).isGreaterThan( ordinalLevel3 );
136+
ordinalLevel3 = level3.getOrdinal();
137+
}
138+
}
139+
} );
140+
} );
141+
}
142+
143+
@BeforeAll
144+
public void setUp(SessionFactoryScope scope) {
145+
scope.inTransaction( session -> {
146+
147+
final Iterator<Long> randomOrdinals = new Random().longs( 100, 999 )
148+
.distinct().limit( 200 ).boxed().iterator();
149+
150+
for ( long l1 = 1; l1 <= 5; l1++ ) {
151+
final Level1 root = new Level1( l1 );
152+
153+
for ( long l2 = 1; l2 <= 5; l2++ ) {
154+
final long l2Id = (l1 * 10) + l2;
155+
final Level2 child2 = new Level2( root, l2Id, randomOrdinals.next() );
156+
157+
for ( long l3 = 1; l3 <= 5; l3++ ) {
158+
final long l3Id = (l2Id * 10) + l3;
159+
new Level3( child2, l3Id, randomOrdinals.next() );
160+
}
161+
}
162+
session.persist( root );
163+
}
164+
} );
165+
}
166+
167+
@AfterAll
168+
public void tearDown(SessionFactoryScope scope) {
169+
scope.getSessionFactory().getSchemaManager().truncateMappedObjects();
170+
}
171+
172+
@Entity(name = "Level1")
173+
@NamedEntityGraphs({
174+
@NamedEntityGraph(
175+
name = "level1_loadAll",
176+
attributeNodes = {
177+
@NamedAttributeNode(value = "children", subgraph = "subgraph.children")
178+
},
179+
subgraphs = {
180+
@NamedSubgraph(
181+
name = "subgraph.children",
182+
attributeNodes = {
183+
@NamedAttributeNode(value = "children")
184+
}
185+
)
186+
}
187+
)
188+
})
189+
static class Level1 {
190+
@Id
191+
private Long id;
192+
193+
@OneToMany(fetch = FetchType.LAZY,
194+
mappedBy = "parent",
195+
cascade = CascadeType.PERSIST)
196+
@OrderBy("ordinal")
197+
private Set<Level2> children = new LinkedHashSet<>();
198+
199+
public Level1() {
200+
}
201+
202+
public Level1(Long id) {
203+
this.id = id;
204+
}
205+
206+
public Long getId() {
207+
return id;
208+
}
209+
210+
public void setId(Long id) {
211+
this.id = id;
212+
}
213+
214+
public Set<Level2> getChildren() {
215+
return children;
216+
}
217+
218+
@Override
219+
public String toString() {
220+
return "Level1 #" + id;
221+
}
222+
}
223+
224+
@Entity(name = "Level2")
225+
static class Level2 {
226+
@Id
227+
Long id;
228+
229+
Long ordinal;
230+
231+
@ManyToOne(fetch = FetchType.LAZY)
232+
@JoinColumn(name = "parent_id")
233+
private Level1 parent;
234+
235+
@OneToMany(fetch = FetchType.LAZY,
236+
mappedBy = "parent",
237+
cascade = CascadeType.PERSIST)
238+
@OrderBy("ordinal")
239+
private Set<Level3> children = new LinkedHashSet<>();
240+
241+
public Level2() {
242+
}
243+
244+
public Level2(Level1 parent, Long id, Long ordinal) {
245+
this.parent = parent;
246+
this.id = id;
247+
this.ordinal = ordinal;
248+
parent.getChildren().add( this );
249+
}
250+
251+
public Long getId() {
252+
return id;
253+
}
254+
255+
public void setId(Long id) {
256+
this.id = id;
257+
}
258+
259+
public Level1 getParent() {
260+
return parent;
261+
}
262+
263+
public void setParent(Level1 parent) {
264+
this.parent = parent;
265+
}
266+
267+
public Set<Level3> getChildren() {
268+
return children;
269+
}
270+
271+
public Long getOrdinal() {
272+
return ordinal;
273+
}
274+
275+
@Override
276+
public String toString() {
277+
return "Level1 #" + id + " $" + ordinal;
278+
}
279+
}
280+
281+
@Entity(name = "Level3")
282+
static class Level3 {
283+
@Id
284+
Long id;
285+
286+
Long ordinal;
287+
288+
@ManyToOne(fetch = FetchType.LAZY)
289+
@JoinColumn(name = "parent_id")
290+
private Level2 parent;
291+
292+
public Level3() {
293+
}
294+
295+
public Level3(Level2 parent, Long id, Long ordinal) {
296+
this.parent = parent;
297+
this.id = id;
298+
this.ordinal = ordinal;
299+
parent.getChildren().add( this );
300+
}
301+
302+
public Long getId() {
303+
return id;
304+
}
305+
306+
public void setId(Long id) {
307+
this.id = id;
308+
}
309+
310+
public Level2 getParent() {
311+
return parent;
312+
}
313+
314+
public void setParent(Level2 parent) {
315+
this.parent = parent;
316+
}
317+
318+
public Long getOrdinal() {
319+
return ordinal;
320+
}
321+
322+
@Override
323+
public String toString() {
324+
return "Level3 #" + id + " $" + ordinal;
325+
}
326+
}
327+
}

0 commit comments

Comments
 (0)