-
Notifications
You must be signed in to change notification settings - Fork 149
/
JoinFetch.java
155 lines (131 loc) · 5.68 KB
/
JoinFetch.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* Copyright 2014-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.kaczmarzyk.spring.data.jpa.domain;
import static net.kaczmarzyk.spring.data.jpa.utils.JoinPathUtils.pathToJoinContainsAlias;
import static net.kaczmarzyk.spring.data.jpa.utils.JoinPathUtils.pathToJoinSplittedByDot;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Fetch;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import net.kaczmarzyk.spring.data.jpa.utils.QueryContext;
/**
* <p>Extension to specification-arg-resolver to allow fetching collections in a specification query</p>
*
* @author Tomasz Kaczmarzyk
* @author Gerald Humphries
* @author Jakub Radlica
*/
public class JoinFetch<T> implements Specification<T>, Fake {
private static final long serialVersionUID = 1L;
private QueryContext context;
private List<String> pathsToFetch;
private String alias;
private JoinType joinType;
private boolean distinct;
public JoinFetch(QueryContext queryContext, String[] pathsToFetch, JoinType joinType, boolean distinct) {
this(queryContext, pathsToFetch, "", joinType, distinct);
}
public JoinFetch(QueryContext queryContext, String[] pathsToFetch, String alias, JoinType joinType, boolean distinct) {
this.context = queryContext;
this.pathsToFetch = Arrays.asList(pathsToFetch);
this.alias = alias;
this.joinType = joinType;
this.distinct = distinct;
if (!alias.isEmpty() && pathsToFetch.length != 1) {
throw new IllegalArgumentException(
"Join fetch alias can be defined only for join fetch with a single path! " +
"Remove alias from the annotation or repeat @JoinFetch annotation for every path and use unique alias for each join."
);
}
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.distinct(distinct);
if (!Number.class.isAssignableFrom(query.getResultType())) { // if it's not a count query, then just execute the fetch
if (pathsToFetch.size() == 1) {
String pathToFetch = pathsToFetch.get(0);
if (pathToJoinContainsAlias(pathToFetch)) {
String[] pathToJoinFetchOnSplittedByDot = pathToJoinSplittedByDot(pathToFetch);
String alias = pathToJoinFetchOnSplittedByDot[0];
String path = pathToJoinFetchOnSplittedByDot[1];
Fetch<?, ?> evaluatedJoinFetchForGivenAlias = context.getEvaluatedJoinFetch(alias);
if(evaluatedJoinFetchForGivenAlias == null) {
throw new IllegalArgumentException(
"Join fetch definition with alias: '" + alias + "' not found! " +
"Make sure that join with the alias '" + alias +"' is defined before the join with path: '" + pathToFetch + "'"
);
}
Fetch<?,?> joinFetch = evaluatedJoinFetchForGivenAlias.fetch(path, joinType);
if (StringUtils.isNotBlank(this.alias)) {
context.putEvaluatedJoinFetch(this.alias, joinFetch);
}
} else {
Fetch<Object, Object> evaluated = root.fetch(pathToFetch, joinType);
context.putEvaluatedJoinFetch(alias, evaluated);
}
} else {
for (String path : pathsToFetch) {
root.fetch(path, joinType);
}
}
} else { // count query -- join fetch can be skipped unless it is used not only for fetching but for filtering as well
if (!alias.isEmpty()) { // assumption: presence of a non-empty alias means that join fetch is used for filtering as well
// unfortunately, Hibernate disallows adding join fetches to count queries
// (or more specifcally, does not allow fetching if fetch-root is not present in the query result)
// so we need to convert the join fetch into a regular join.
// In case that an alias exist, but is not used, then join won't be applied (as joins are lazily evaluated by the lib)
// so theoretically we could skip this if and let Join logic just work, but keeping it can hopefully reduce potential for hard-to-debug errors
String pathToJoin = pathsToFetch.iterator().next(); // see the constructor, if alias is used, then pathsToFetch must have size 1
Join<T> regularJoin = new Join<T>(context, pathToJoin, alias, joinType, distinct);
return regularJoin.toPredicate(root, query, cb);
}
}
return null;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JoinFetch<?> joinFetch = (JoinFetch<?>) o;
return distinct == joinFetch.distinct &&
Objects.equals(context, joinFetch.context) &&
Objects.equals(pathsToFetch, joinFetch.pathsToFetch) &&
Objects.equals(alias, joinFetch.alias) &&
joinType == joinFetch.joinType;
}
@Override
public int hashCode() {
return Objects.hash(context, pathsToFetch, alias, joinType, distinct);
}
@Override
public String toString() {
return "JoinFetch[" +
"pathsToFetch=" + pathsToFetch +
", joinType=" + joinType +
']';
}
}