Skip to content

Commit 2422028

Browse files
[fix] Remove column duplicates when using join fetch (darrachequesne#64)
Previously, a @onetomany relationship could trigger multiple join fetch. Now, there is a maximum of two 'LEFT JOIN' clauses, one for filtering and one for fetching. This was made possible by converting the input to a tree. Example: - name - company.id - company.name - company.description - company.ceo.name - company.ceo.age Tree: - name - company --- name --- description --- ceo ----- name ----- age Closes darrachequesne#58
1 parent 34c9eda commit 2422028

20 files changed

+737
-447
lines changed

pom.xml

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,9 @@
33
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
44
<modelVersion>4.0.0</modelVersion>
55

6-
<parent>
7-
<groupId>io.spring.platform</groupId>
8-
<artifactId>platform-bom</artifactId>
9-
<version>Brussels-RELEASE</version>
10-
</parent>
11-
126
<groupId>com.github.darrachequesne</groupId>
137
<artifactId>spring-data-jpa-datatables</artifactId>
14-
<version>4.1</version>
8+
<version>4.2-SNAPSHOT</version>
159

1610
<name>Spring Data JPA for DataTables</name>
1711
<description>Spring Data JPA extension to work with the great jQuery plug-in DataTables (http://datatables.net/)</description>
@@ -65,6 +59,18 @@
6559
<source.encoding>UTF-8</source.encoding>
6660
</properties>
6761

62+
<dependencyManagement>
63+
<dependencies>
64+
<dependency>
65+
<groupId>io.spring.platform</groupId>
66+
<artifactId>platform-bom</artifactId>
67+
<version>Brussels-RELEASE</version>
68+
<type>pom</type>
69+
<scope>import</scope>
70+
</dependency>
71+
</dependencies>
72+
</dependencyManagement>
73+
6874
<dependencies>
6975
<dependency>
7076
<groupId>org.projectlombok</groupId>
@@ -221,9 +227,6 @@
221227
<plugin>
222228
<groupId>org.apache.maven.plugins</groupId>
223229
<artifactId>maven-source-plugin</artifactId>
224-
<configuration>
225-
<encoding>${source.encoding}</encoding>
226-
</configuration>
227230
</plugin>
228231
<plugin>
229232
<groupId>org.apache.maven.plugins</groupId>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.springframework.data.jpa.datatables;
2+
3+
import org.springframework.data.domain.Pageable;
4+
import org.springframework.data.domain.Sort;
5+
import org.springframework.data.jpa.datatables.mapping.Column;
6+
import org.springframework.data.jpa.datatables.mapping.DataTablesInput;
7+
import org.springframework.data.jpa.datatables.mapping.Search;
8+
import org.springframework.util.StringUtils;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
13+
abstract class AbstractPredicateBuilder<T> {
14+
protected final DataTablesInput input;
15+
final boolean hasGlobalFilter;
16+
final Node<Filter> tree;
17+
18+
AbstractPredicateBuilder(DataTablesInput input) {
19+
this.input = input;
20+
this.hasGlobalFilter = input.getSearch() != null && StringUtils.hasText(input.getSearch().getValue());
21+
if (this.hasGlobalFilter) {
22+
tree = new Node<Filter>(null, new GlobalFilter(input.getSearch().getValue()));
23+
} else {
24+
tree = new Node<Filter>(null);
25+
}
26+
initTree(input);
27+
}
28+
29+
private void initTree(DataTablesInput input) {
30+
for (Column column : input.getColumns()) {
31+
if (column.getSearchable()) {
32+
addChild(tree, 0, column.getData().split("\\."), column.getSearch());
33+
}
34+
}
35+
}
36+
37+
private void addChild(Node<Filter> parent, int index, String[] names, Search search) {
38+
boolean isLast = index + 1 == names.length;
39+
if (isLast) {
40+
boolean hasColumnFilter = search != null && StringUtils.hasText(search.getValue());
41+
parent.addChild(new Node<Filter>(names[index], hasColumnFilter ? new ColumnFilter(search.getValue()) : null));
42+
} else {
43+
Node<Filter> child = parent.getOrCreateChild(names[index]);
44+
addChild(child, index + 1, names, search);
45+
}
46+
}
47+
48+
/**
49+
* Creates a 'LIMIT .. OFFSET .. ORDER BY ..' clause for the given {@link DataTablesInput}.
50+
*
51+
* @return a {@link Pageable}, must not be {@literal null}.
52+
*/
53+
public Pageable createPageable() {
54+
List<Sort.Order> orders = new ArrayList<Sort.Order>();
55+
for (org.springframework.data.jpa.datatables.mapping.Order order : input.getOrder()) {
56+
Column column = input.getColumns().get(order.getColumn());
57+
if (column.getOrderable()) {
58+
String sortColumn = column.getData();
59+
Sort.Direction sortDirection = Sort.Direction.fromString(order.getDir());
60+
orders.add(new Sort.Order(sortDirection, sortColumn));
61+
}
62+
}
63+
Sort sort = orders.isEmpty() ? null : new Sort(orders);
64+
65+
if (input.getLength() == -1) {
66+
input.setStart(0);
67+
input.setLength(Integer.MAX_VALUE);
68+
}
69+
return new DataTablesPageRequest(input.getStart(), input.getLength(), sort);
70+
}
71+
72+
public abstract T build();
73+
74+
private class DataTablesPageRequest implements Pageable {
75+
private final int offset;
76+
private final int pageSize;
77+
private final Sort sort;
78+
79+
DataTablesPageRequest(int offset, int pageSize, Sort sort) {
80+
this.offset = offset;
81+
this.pageSize = pageSize;
82+
this.sort = sort;
83+
}
84+
85+
@Override
86+
public int getOffset() {
87+
return offset;
88+
}
89+
90+
@Override
91+
public int getPageSize() {
92+
return pageSize;
93+
}
94+
95+
@Override
96+
public Sort getSort() {
97+
return sort;
98+
}
99+
100+
@Override
101+
public Pageable next() {
102+
throw new UnsupportedOperationException();
103+
}
104+
105+
@Override
106+
public Pageable previousOrFirst() {
107+
throw new UnsupportedOperationException();
108+
}
109+
110+
@Override
111+
public Pageable first() {
112+
throw new UnsupportedOperationException();
113+
}
114+
115+
@Override
116+
public boolean hasPrevious() {
117+
throw new UnsupportedOperationException();
118+
}
119+
120+
@Override
121+
public int getPageNumber() {
122+
throw new UnsupportedOperationException();
123+
}
124+
}
125+
126+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.springframework.data.jpa.datatables;
2+
3+
import com.querydsl.core.types.Ops;
4+
import com.querydsl.core.types.Predicate;
5+
import com.querydsl.core.types.dsl.*;
6+
7+
import javax.persistence.criteria.CriteriaBuilder;
8+
import javax.persistence.criteria.Expression;
9+
import javax.persistence.criteria.From;
10+
import java.util.HashSet;
11+
import java.util.Set;
12+
13+
import static java.util.Collections.unmodifiableSet;
14+
15+
/**
16+
* Filter which parses the input value to create one of the following predicates:
17+
* <ul>
18+
* <li>WHERE ... LIKE ..., see {@link GlobalFilter}</li>
19+
* <li>WHERE ... IN ... when the input contains multiple values separated by "+"</li>
20+
* <li>WHERE ... IS NULL when the input is equals to "NULL"</li>
21+
* <li>WHERE ... IN ... OR ... IS NULL</li>
22+
* </ul>
23+
*/
24+
class ColumnFilter extends GlobalFilter {
25+
private Set<String> values = new HashSet<String>();
26+
private Set<Boolean> booleanValues = new HashSet<Boolean>();
27+
private boolean addNullCase;
28+
private boolean isBooleanComparison;
29+
30+
ColumnFilter(String filterValue) {
31+
super(filterValue);
32+
33+
isBooleanComparison = true;
34+
for (String value : filterValue.split("\\+")) {
35+
if ("NULL".equals(value)) {
36+
addNullCase = true;
37+
} else {
38+
isBooleanComparison &= isBoolean(value);
39+
values.add(nullOrTrimmedValue(value));
40+
}
41+
}
42+
values = unmodifiableSet(values);
43+
44+
if (isBooleanComparison) {
45+
for (String value : values) {
46+
booleanValues.add(Boolean.valueOf(value));
47+
}
48+
booleanValues = unmodifiableSet(booleanValues);
49+
}
50+
}
51+
52+
private boolean isBoolean(String filterValue) {
53+
return "TRUE".equalsIgnoreCase(filterValue) || "FALSE".equalsIgnoreCase(filterValue);
54+
}
55+
56+
@Override
57+
public Predicate createPredicate(PathBuilder<?> pathBuilder, String attributeName) {
58+
StringOperation path = Expressions.stringOperation(Ops.STRING_CAST, pathBuilder.get(attributeName));
59+
BooleanPath booleanPath = pathBuilder.getBoolean(attributeName);
60+
61+
if (values.isEmpty()) {
62+
return addNullCase ? path.isNull() : null;
63+
} else if (isBasicFilter()) {
64+
return super.createPredicate(pathBuilder, attributeName);
65+
}
66+
67+
BooleanExpression predicate = isBooleanComparison ? booleanPath.in(booleanValues) : path.in(values);
68+
if (addNullCase) predicate = predicate.or(path.isNull());
69+
return predicate;
70+
}
71+
72+
@Override
73+
public javax.persistence.criteria.Predicate createPredicate(From<?, ?> from, CriteriaBuilder criteriaBuilder, String attributeName) {
74+
Expression<?> expression = from.get(attributeName);
75+
76+
if (values.isEmpty()) {
77+
return addNullCase ? expression.isNull() : criteriaBuilder.conjunction();
78+
} else if (isBasicFilter()) {
79+
return super.createPredicate(from, criteriaBuilder, attributeName);
80+
}
81+
82+
javax.persistence.criteria.Predicate predicate;
83+
if (isBooleanComparison) {
84+
predicate = expression.as(Boolean.class).in(booleanValues);
85+
} else {
86+
predicate = expression.as(String.class).in(values);
87+
}
88+
if (addNullCase) predicate = criteriaBuilder.or(predicate, expression.isNull());
89+
90+
return predicate;
91+
}
92+
93+
private boolean isBasicFilter() {
94+
return values.size() == 1 && !addNullCase && !isBooleanComparison;
95+
}
96+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.springframework.data.jpa.datatables;
2+
3+
import com.querydsl.core.types.dsl.PathBuilder;
4+
5+
import javax.persistence.criteria.CriteriaBuilder;
6+
import javax.persistence.criteria.From;
7+
import javax.persistence.criteria.Predicate;
8+
9+
interface Filter {
10+
11+
Predicate createPredicate(From<?, ?> from, CriteriaBuilder criteriaBuilder, String attributeName);
12+
13+
com.querydsl.core.types.Predicate createPredicate(PathBuilder<?> pathBuilder, String attributeName);
14+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.springframework.data.jpa.datatables;
2+
3+
import com.querydsl.core.types.Ops;
4+
import com.querydsl.core.types.dsl.Expressions;
5+
import com.querydsl.core.types.dsl.PathBuilder;
6+
import com.querydsl.core.types.dsl.StringOperation;
7+
8+
import javax.persistence.criteria.CriteriaBuilder;
9+
import javax.persistence.criteria.Expression;
10+
import javax.persistence.criteria.From;
11+
import javax.persistence.criteria.Predicate;
12+
13+
/**
14+
* Filter which creates a basic "WHERE ... LIKE ..." clause
15+
*/
16+
class GlobalFilter implements Filter {
17+
private final String escapedRawValue;
18+
19+
GlobalFilter(String filterValue) {
20+
escapedRawValue = escapeValue(filterValue);
21+
}
22+
23+
String nullOrTrimmedValue(String value) {
24+
return "\\NULL".equals(value) ? "NULL" : value.trim();
25+
}
26+
27+
private String escapeValue(String filterValue) {
28+
return "%" + nullOrTrimmedValue(filterValue).toLowerCase()
29+
.replaceAll("~", "~~")
30+
.replaceAll("%", "~%")
31+
.replaceAll("_", "~_") + "%";
32+
}
33+
34+
@Override
35+
public Predicate createPredicate(From<?, ?> from, CriteriaBuilder criteriaBuilder, String attributeName) {
36+
Expression<?> expression = from.get(attributeName);
37+
return criteriaBuilder.like(criteriaBuilder.lower(expression.as(String.class)), escapedRawValue, '~');
38+
}
39+
40+
@Override
41+
public com.querydsl.core.types.Predicate createPredicate(PathBuilder<?> pathBuilder, String attributeName) {
42+
StringOperation path = Expressions.stringOperation(Ops.STRING_CAST, pathBuilder.get(attributeName));
43+
return path.lower().like(escapedRawValue, '~');
44+
}
45+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.springframework.data.jpa.datatables;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
class Node<T> {
7+
private final String name;
8+
private final T data;
9+
private List<Node<T>> children = new ArrayList<Node<T>>();
10+
11+
Node(String name, T data) {
12+
this.name = name;
13+
this.data = data;
14+
}
15+
16+
Node(String name) {
17+
this.name = name;
18+
this.data = null;
19+
}
20+
21+
void addChild(Node<T> child) {
22+
children.add(child);
23+
}
24+
25+
Node<T> getOrCreateChild(String name) {
26+
for (Node<T> child : children) {
27+
if (child.name.equals(name)) {
28+
return child;
29+
}
30+
}
31+
Node<T> child = new Node<T>(name);
32+
children.add(child);
33+
return child;
34+
}
35+
36+
boolean isLeaf() {
37+
return this.children.isEmpty();
38+
}
39+
40+
public T getData() {
41+
return data;
42+
}
43+
44+
public String getName() {
45+
return name;
46+
}
47+
48+
List<Node<T>> getChildren() {
49+
return children;
50+
}
51+
}

0 commit comments

Comments
 (0)