Skip to content

Commit 4656272

Browse files
[fix] Add proper JOIN FETCH clause (darrachequesne#67)
There was a regression in 2422028, where a JOIN FETCH would be added for `@OneToMany` relationships, triggering the dreaded "firstResult/maxResults specified with collection fetch; applying in memory".
1 parent dfa5f10 commit 4656272

File tree

9 files changed

+281
-19
lines changed

9 files changed

+281
-19
lines changed

src/main/java/org/springframework/data/jpa/datatables/SpecificationBuilder.java

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.springframework.data.jpa.datatables;
22

3+
import org.hibernate.jpa.criteria.path.AbstractPathImpl;
34
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
45
import org.springframework.data.jpa.domain.Specification;
56

@@ -24,17 +25,12 @@ private class DataTablesSpecification<S> implements Specification<S> {
2425

2526
@Override
2627
public Predicate toPredicate(Root<S> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
27-
initPredicatesRecursively(tree, root, criteriaBuilder);
28-
29-
boolean isCountQuery = query.getResultType() == Long.class;
30-
if (!isCountQuery) {
31-
addFetchRecursively(tree, root);
32-
}
28+
initPredicatesRecursively(tree, root, root, criteriaBuilder);
3329

3430
return createFinalPredicate(criteriaBuilder);
3531
}
3632

37-
private void initPredicatesRecursively(Node<Filter> node, From<S, S> from, CriteriaBuilder criteriaBuilder) {
33+
private void initPredicatesRecursively(Node<Filter> node, From<S, S> from, FetchParent<S, S> fetch, CriteriaBuilder criteriaBuilder) {
3834
if (node.isLeaf()) {
3935
boolean hasColumnFilter = node.getData() != null;
4036
if (hasColumnFilter) {
@@ -46,19 +42,20 @@ private void initPredicatesRecursively(Node<Filter> node, From<S, S> from, Crite
4642
}
4743
}
4844
for (Node<Filter> child : node.getChildren()) {
49-
From<S, S> childFrom = from;
50-
if (!child.isLeaf()) {
51-
childFrom = from.join(child.getName(), JoinType.LEFT);
45+
Path<Object> path = from.get(child.getName());
46+
if (path instanceof AbstractPathImpl) {
47+
if (((AbstractPathImpl) path).getAttribute().isCollection()) {
48+
// ignore OneToMany and ManyToMany relationships
49+
continue;
50+
}
51+
}
52+
if (child.isLeaf()) {
53+
initPredicatesRecursively(child, from, fetch, criteriaBuilder);
54+
} else {
55+
Join<S, S> join = from.join(child.getName(), JoinType.LEFT);
56+
Fetch<S, S> childFetch = fetch.fetch(child.getName(), JoinType.LEFT);
57+
initPredicatesRecursively(child, join, childFetch, criteriaBuilder);
5258
}
53-
initPredicatesRecursively(child, childFrom, criteriaBuilder);
54-
}
55-
}
56-
57-
private void addFetchRecursively(Node<?> node, FetchParent<S, S> fetch) {
58-
for (Node<?> child : node.getChildren()) {
59-
if (child.isLeaf()) continue;
60-
FetchParent<S, S> childFetch = fetch.fetch(child.getName(), JoinType.LEFT);
61-
addFetchRecursively(child, childFetch);
6259
}
6360
}
6461

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.springframework.data.jpa.datatables.model;
2+
3+
import lombok.*;
4+
import lombok.experimental.Tolerate;
5+
6+
import javax.persistence.*;
7+
import java.util.Collections;
8+
import java.util.List;
9+
10+
import static java.util.Arrays.asList;
11+
import static java.util.Collections.singletonList;
12+
13+
@Data
14+
@AllArgsConstructor
15+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
16+
@EqualsAndHashCode(exclude = "b")
17+
@ToString(exclude = "b")
18+
@Builder
19+
@Entity
20+
@Table(name = "a")
21+
public class A {
22+
23+
private @Id String name;
24+
25+
@OneToMany(cascade = CascadeType.ALL)
26+
@JoinColumn(name="a_id")
27+
private List<B> b;
28+
29+
@ManyToOne(cascade = CascadeType.ALL)
30+
@JoinColumn(name = "c_id")
31+
private C c;
32+
33+
@Embedded
34+
private D d;
35+
36+
private static C C1 = new C("C1", "VAL1", new C("C3", "VAL3", null));
37+
private static C C2 = new C("C2", "VAL2", null);
38+
39+
public static A A1 = A.builder()
40+
.name("A1")
41+
.b(asList(
42+
new B("B1", "VAL1"),
43+
new B("B2", "VAL2")
44+
))
45+
.c(C1)
46+
.d(new D("D1"))
47+
.build();
48+
49+
public static A A2 = A.builder()
50+
.name("A2")
51+
.b(singletonList(
52+
new B("B3", "VAL3")
53+
))
54+
.c(C2)
55+
.d(new D("D2"))
56+
.build();
57+
58+
public static A A3 = A.builder()
59+
.name("A3")
60+
.b(Collections.<B>emptyList())
61+
.c(C2)
62+
.build();
63+
64+
public static List<A> ALL = asList(
65+
A1,
66+
A2,
67+
A3
68+
);
69+
70+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.springframework.data.jpa.datatables.model;
2+
3+
import lombok.AccessLevel;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import javax.persistence.Entity;
9+
import javax.persistence.Id;
10+
import javax.persistence.Table;
11+
12+
@Data
13+
@AllArgsConstructor
14+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
15+
@Entity
16+
@Table(name = "b")
17+
class B {
18+
private @Id String name;
19+
private String value;
20+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.springframework.data.jpa.datatables.model;
2+
3+
import lombok.AccessLevel;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
import lombok.experimental.Tolerate;
8+
9+
import javax.persistence.*;
10+
11+
@Data
12+
@AllArgsConstructor
13+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
14+
@Entity
15+
@Table(name = "c")
16+
class C {
17+
18+
private @Id String name;
19+
20+
private String value;
21+
22+
@ManyToOne(cascade = CascadeType.ALL)
23+
@JoinColumn(name = "parent_id")
24+
private C parent;
25+
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.springframework.data.jpa.datatables.model;
2+
3+
import lombok.AccessLevel;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
import lombok.experimental.Tolerate;
8+
9+
import javax.persistence.Embeddable;
10+
11+
@Data
12+
@AllArgsConstructor
13+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
14+
@Embeddable
15+
class D {
16+
private String value;
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.springframework.data.jpa.datatables.qrepository;
2+
3+
import org.springframework.data.jpa.datatables.model.A;
4+
5+
public interface QRelationshipsRepository extends QDataTablesRepository<A, String> {
6+
7+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.springframework.data.jpa.datatables.qrepository;
2+
3+
import org.junit.Ignore;
4+
import org.junit.Test;
5+
import org.junit.runner.RunWith;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.data.jpa.datatables.Config;
8+
import org.springframework.data.jpa.datatables.QConfig;
9+
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
10+
import org.springframework.data.jpa.datatables.mapping.DataTablesOutput;
11+
import org.springframework.data.jpa.datatables.model.A;
12+
import org.springframework.data.jpa.datatables.repository.RelationshipsRepositoryTest;
13+
import org.springframework.test.context.ContextConfiguration;
14+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
15+
16+
@RunWith(SpringJUnit4ClassRunner.class)
17+
@ContextConfiguration(classes = {Config.class, QConfig.class})
18+
public class QRelationshipsRepositoryTest extends RelationshipsRepositoryTest {
19+
@Autowired
20+
private QRelationshipsRepository repository;
21+
22+
@Override
23+
protected DataTablesOutput<A> getOutput(DataTablesInput input) {
24+
return repository.findAll(input);
25+
}
26+
27+
@Test
28+
@Ignore
29+
@Override
30+
public void checkFetchJoin() {
31+
// see https://github.com/darrachequesne/spring-data-jpa-datatables/issues/7
32+
}
33+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.springframework.data.jpa.datatables.repository;
2+
3+
import org.springframework.data.jpa.datatables.model.A;
4+
5+
interface RelationshipsRepository extends DataTablesRepository<A, String> {
6+
7+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.springframework.data.jpa.datatables.repository;
2+
3+
import org.hibernate.SessionFactory;
4+
import org.hibernate.stat.Statistics;
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.data.jpa.datatables.Config;
10+
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
11+
import org.springframework.data.jpa.datatables.mapping.DataTablesOutput;
12+
import org.springframework.data.jpa.datatables.model.A;
13+
import org.springframework.test.context.ContextConfiguration;
14+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
@RunWith(SpringJUnit4ClassRunner.class)
19+
@ContextConfiguration(classes = Config.class)
20+
public class RelationshipsRepositoryTest {
21+
protected DataTablesInput input;
22+
23+
@Autowired
24+
private RelationshipsRepository repository;
25+
26+
@Autowired
27+
private SessionFactory sessionFactory;
28+
29+
protected DataTablesOutput<A> getOutput(DataTablesInput input) {
30+
return repository.findAll(input);
31+
}
32+
33+
@Before
34+
public void init() {
35+
repository.deleteAll();
36+
repository.save(A.ALL);
37+
input = getBasicInput();
38+
}
39+
40+
@Test
41+
public void manyToOne() {
42+
input.getColumn("c.value").setSearchValue("VAL2");
43+
DataTablesOutput<A> output = getOutput(input);
44+
assertThat(output.getData()).containsOnly(A.A2, A.A3);
45+
}
46+
47+
@Test
48+
public void twoLevels() {
49+
input.getColumn("c.parent.value").setSearchValue("VAL3");
50+
DataTablesOutput<A> output = getOutput(input);
51+
assertThat(output.getData()).containsOnly(A.A1);
52+
}
53+
54+
@Test
55+
public void embedded() {
56+
input.getColumn("d.value").setSearchValue("D1");
57+
DataTablesOutput<A> output = getOutput(input);
58+
assertThat(output.getData()).containsOnly(A.A1);
59+
}
60+
61+
@Test
62+
public void checkFetchJoin() {
63+
Statistics statistics = sessionFactory.getStatistics();
64+
statistics.setStatisticsEnabled(true);
65+
66+
DataTablesOutput<A> output = getOutput(input);
67+
68+
assertThat(output.getRecordsFiltered()).isEqualTo(3);
69+
assertThat(statistics.getPrepareStatementCount()).isEqualTo(2);
70+
assertThat(statistics.getEntityLoadCount()).isEqualTo(3 /* A */ + 3 /* C */);
71+
}
72+
73+
private static DataTablesInput getBasicInput() {
74+
DataTablesInput input = new DataTablesInput();
75+
input.addColumn("name", true, true, "");
76+
input.addColumn("b.name", true, true, "");
77+
input.addColumn("b.value", true, true, "");
78+
input.addColumn("c.name", true, true, "");
79+
input.addColumn("c.value", true, true, "");
80+
input.addColumn("c.parent.name", true, true, "");
81+
input.addColumn("c.parent.value", true, true, "");
82+
input.addColumn("d.value", true, true, "");
83+
return input;
84+
}
85+
}

0 commit comments

Comments
 (0)