Skip to content

Commit 4b2eba3

Browse files
committed
HHH-19034 Test fetch and join order for Criteria
1 parent 229e8c1 commit 4b2eba3

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
* Copyright 2014 JBoss Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.hibernate.orm.test.join;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.Objects;
21+
22+
import org.hibernate.Hibernate;
23+
import org.hibernate.metamodel.model.domain.EntityDomainType;
24+
import org.hibernate.query.Order;
25+
26+
import org.hibernate.testing.jdbc.SQLStatementInspector;
27+
import org.hibernate.testing.orm.junit.DomainModel;
28+
import org.hibernate.testing.orm.junit.Jira;
29+
import org.hibernate.testing.orm.junit.SessionFactory;
30+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
31+
import org.junit.BeforeClass;
32+
import org.junit.jupiter.api.AfterEach;
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.Test;
36+
37+
import jakarta.persistence.Entity;
38+
import jakarta.persistence.Id;
39+
import jakarta.persistence.OneToMany;
40+
import jakarta.persistence.criteria.CriteriaBuilder;
41+
import jakarta.persistence.criteria.CriteriaQuery;
42+
import jakarta.persistence.criteria.Fetch;
43+
import jakarta.persistence.criteria.Join;
44+
import jakarta.persistence.criteria.Root;
45+
import jakarta.persistence.metamodel.SingularAttribute;
46+
47+
import static org.assertj.core.api.Assertions.assertThat;
48+
49+
/**
50+
* When fetching and joining associations using the criteria API, we need to check that we are not affected by the order
51+
* of the operations
52+
*/
53+
@SessionFactory
54+
@DomainModel(annotatedClasses = { JoinAndFetchWithCriteriaSelectionQueryTest.Book.class, JoinAndFetchWithCriteriaSelectionQueryTest.Author.class })
55+
@Jira("https://hibernate.atlassian.net/browse/HHH-19034")
56+
class JoinAndFetchWithCriteriaSelectionQueryTest {
57+
58+
static final Author amal = new Author( 1L, "Amal El-Mohtar" );
59+
static final Author max = new Author( 2L, "Max Gladstone" );
60+
static final Author ursula = new Author( 3L, "Ursula K. Le Guin" );
61+
static final Book timeWar = new Book( 1L, "This Is How You Lose the Time War" );
62+
static final Book leftHand = new Book( 2L, "The Left Hand of Darkness" );
63+
64+
@BeforeAll
65+
public static void populateDb(SessionFactoryScope scope) {
66+
timeWar.getAuthors().add( amal );
67+
timeWar.getAuthors().add( max );
68+
leftHand.getAuthors().add( ursula );
69+
scope.inTransaction( session -> {
70+
session.persist( amal );
71+
session.persist( max );
72+
session.persist( ursula );
73+
session.persist( timeWar );
74+
session.persist( leftHand );
75+
} );
76+
}
77+
78+
@Test
79+
void fetchBeforeJoinWithWhereClauseTest(SessionFactoryScope scope) {
80+
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
81+
inspector.clear();
82+
scope.inTransaction( session -> {
83+
// Find all the books from an author, and load the authors association eagerly
84+
CriteriaBuilder cb = session.getCriteriaBuilder();
85+
CriteriaQuery<Book> query = cb.createQuery( Book.class );
86+
Root<Book> from = query.from( Book.class );
87+
88+
// The fetch MUST BE created before the join for this test
89+
Fetch<Object, Object> fetch = from.fetch( "authors" );
90+
Join<Object, Object> join = from.join( "authors" );
91+
query.where( cb.equal( join.get( "id" ), 2L ) );
92+
93+
// Because there's a filter on the association, they need to be two distinct joins
94+
assertThat( join ).isNotEqualTo( fetch );
95+
96+
Book book = session.createQuery( query ).getSingleResult();
97+
assertThat( book ).isEqualTo( timeWar );
98+
assertThat( Hibernate.isInitialized( book.getAuthors() ) ).isTrue();
99+
assertThat( book.getAuthors() ).containsExactlyInAnyOrder( amal, max );
100+
inspector.assertExecutedCount( 1 );
101+
inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 );
102+
} );
103+
}
104+
105+
@Test
106+
void fetchAfterJoinWithWhereClauseTest(SessionFactoryScope scope) {
107+
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
108+
inspector.clear();
109+
scope.inTransaction( session -> {
110+
// Find all the books from an author, and load the authors association eagerly
111+
CriteriaBuilder cb = session.getCriteriaBuilder();
112+
CriteriaQuery<Book> query = cb.createQuery( Book.class );
113+
Root<Book> from = query.from( Book.class );
114+
115+
// The join MUST BE created before the fetch for this test
116+
Join<Object, Object> join = from.join( "authors" );
117+
Fetch<Object, Object> fetch = from.fetch( "authors" );
118+
query.where( cb.equal( join.get( "id" ), 2L ) );
119+
120+
// Because there's a filter on the association, they need to be two distinct joins
121+
assertThat( join ).isNotEqualTo( fetch );
122+
Book book = session.createQuery( query ).getSingleResult();
123+
124+
assertThat( book ).isEqualTo( timeWar );
125+
assertThat( Hibernate.isInitialized( book.getAuthors() ) ).isTrue();
126+
assertThat( book.getAuthors() ).containsExactlyInAnyOrder( amal, max );
127+
inspector.assertExecutedCount( 1 );
128+
inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 );
129+
} );
130+
}
131+
132+
@Test
133+
void fetchAfterJoinWithoutWhereClauseTest(SessionFactoryScope scope) {
134+
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
135+
inspector.clear();
136+
scope.inTransaction( session -> {
137+
CriteriaBuilder cb = session.getCriteriaBuilder();
138+
CriteriaQuery<Book> query = cb.createQuery( Book.class );
139+
Root<Book> from = query.from( Book.class );
140+
141+
// The join MUST BE created before the fetch for this test
142+
Join<Object, Object> join = from.join( "authors" );
143+
Fetch<Object, Object> fetch = from.fetch( "authors" );
144+
145+
// The current behaviour, but we could reuse the same join in this case
146+
assertThat( join ).isNotEqualTo( fetch );
147+
148+
EntityDomainType<Book> bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType( Book.class );
149+
SingularAttribute<? super Book, ?> title = bookType.findSingularAttribute( "title" );
150+
query.select( from ).distinct( true );
151+
152+
List<Book> books = session
153+
.createSelectionQuery( query )
154+
.setOrder( Order.asc( title ) )
155+
.getResultList();
156+
assertThat( books ).containsExactly( leftHand, timeWar );
157+
assertThat( Hibernate.isInitialized( books.get( 0 ).getAuthors() ) ).isTrue();
158+
assertThat( Hibernate.isInitialized( books.get( 1 ).getAuthors() ) ).isTrue();
159+
160+
inspector.assertExecutedCount( 1 );
161+
// The current behaviour, but we could generate a query with only 2 join
162+
inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 );
163+
} );
164+
}
165+
166+
@Test
167+
void fetchBeforeJoinWithoutWhereClauseTest(SessionFactoryScope scope) {
168+
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
169+
inspector.clear();
170+
scope.inTransaction( session -> {
171+
CriteriaBuilder cb = session.getCriteriaBuilder();
172+
CriteriaQuery<Book> query = cb.createQuery( Book.class );
173+
Root<Book> from = query.from( Book.class );
174+
175+
// The fetch MUST BE created before the join for this test
176+
Fetch<Object, Object> fetch = from.fetch( "authors" );
177+
Join<Object, Object> join = from.join( "authors" );
178+
179+
// The current behaviour, but we could reuse the same join in this case
180+
assertThat( join ).isNotEqualTo( fetch );
181+
182+
EntityDomainType<Book> bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType( Book.class );
183+
SingularAttribute<? super Book, ?> title = bookType.findSingularAttribute( "title" );
184+
query.select( from ).distinct( true );
185+
186+
List<Book> books = session
187+
.createSelectionQuery( query )
188+
.setOrder( Order.asc( title ) )
189+
.getResultList();
190+
assertThat( books ).containsExactly( leftHand, timeWar );
191+
assertThat( Hibernate.isInitialized( books.get( 0 ).getAuthors() ) ).isTrue();
192+
assertThat( Hibernate.isInitialized( books.get( 1 ).getAuthors() ) ).isTrue();
193+
194+
inspector.assertExecutedCount( 1 );
195+
// The current behaviour, but we could generate a query with only 2 join
196+
inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 );
197+
} );
198+
}
199+
200+
@Entity(name = "Author")
201+
public static class Author {
202+
@Id
203+
private Long id;
204+
private String name;
205+
206+
public Author() {
207+
}
208+
209+
public Author(Long id, String name) {
210+
this.id = id;
211+
this.name = name;
212+
}
213+
214+
public Long getId() {
215+
return id;
216+
}
217+
218+
public void setId(Long id) {
219+
this.id = id;
220+
}
221+
222+
public String getName() {
223+
return name;
224+
}
225+
226+
public void setName(String name) {
227+
this.name = name;
228+
}
229+
230+
@Override
231+
public boolean equals(Object object) {
232+
if ( object == null || getClass() != object.getClass() ) {
233+
return false;
234+
}
235+
Author author = (Author) object;
236+
return Objects.equals( name, author.name );
237+
}
238+
239+
@Override
240+
public int hashCode() {
241+
return Objects.hashCode( name );
242+
}
243+
244+
@Override
245+
public String toString() {
246+
return id + ":" + name;
247+
}
248+
}
249+
250+
@Entity(name = "Book")
251+
public static class Book {
252+
@Id
253+
private Long id;
254+
private String title;
255+
@OneToMany
256+
private List<Author> authors = new ArrayList<>();
257+
258+
public Book() {
259+
}
260+
261+
public Book(Long id, String title) {
262+
this.id = id;
263+
this.title = title;
264+
}
265+
266+
public Long getId() {
267+
return id;
268+
}
269+
270+
public void setId(Long id) {
271+
this.id = id;
272+
}
273+
274+
public String getTitle() {
275+
return title;
276+
}
277+
278+
public void setTitle(String title) {
279+
this.title = title;
280+
}
281+
282+
public List<Author> getAuthors() {
283+
return authors;
284+
}
285+
286+
public void setAuthors(List<Author> authors) {
287+
this.authors = authors;
288+
}
289+
290+
@Override
291+
public boolean equals(Object object) {
292+
if ( object == null || getClass() != object.getClass() ) {
293+
return false;
294+
}
295+
Book book = (Book) object;
296+
return Objects.equals( title, book.title );
297+
}
298+
299+
@Override
300+
public int hashCode() {
301+
return Objects.hashCode( title );
302+
}
303+
304+
@Override
305+
public String toString() {
306+
return id + ":" + title;
307+
}
308+
}
309+
}

0 commit comments

Comments
 (0)