Skip to content

Commit 879c56b

Browse files
DavideDmbladel
authored andcommitted
HHH-19034 Test fetch and join order for Criteria
1 parent 6a3ef4b commit 879c56b

File tree

1 file changed

+295
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)