Skip to content

Commit dcc3608

Browse files
authored
Merge pull request #195 from croz-ltd/feature_supportLeftJoinInProjectionsAndRestrictions
Add support for specifiying join type for projections and restrictions
2 parents 279566d + a3fe904 commit dcc3608

File tree

9 files changed

+212
-65
lines changed

9 files changed

+212
-65
lines changed

nrich-search-api/src/main/java/net/croz/nrich/search/api/model/SearchConfiguration.java

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import net.croz.nrich.search.api.model.property.SearchPropertyMapping;
2626
import net.croz.nrich.search.api.model.subquery.SubqueryConfiguration;
2727

28+
import jakarta.persistence.criteria.JoinType;
2829
import java.util.List;
2930
import java.util.function.Function;
3031

@@ -103,6 +104,11 @@ public class SearchConfiguration<T, P, R> {
103104
*/
104105
private boolean anyMatch;
105106

107+
/**
108+
* Default join type to be used for projections and conditions. If not specified default is JoinType.INNER
109+
*/
110+
private JoinType defaultJoinType;
111+
106112
@Builder.Default
107113
private SearchPropertyConfiguration searchPropertyConfiguration = SearchPropertyConfiguration.defaultSearchPropertyConfiguration();
108114

nrich-search-api/src/main/java/net/croz/nrich/search/api/model/SearchJoin.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public class SearchJoin<R> {
4747
private String alias;
4848

4949
/**
50-
* Type of join (inner or left).
50+
* Type of join.
5151
*/
5252
private JoinType joinType;
5353

nrich-search-api/src/main/java/net/croz/nrich/search/api/model/subquery/SubqueryConfiguration.java

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import lombok.Setter;
2323
import net.croz.nrich.search.api.model.property.SearchPropertyJoin;
2424

25+
import jakarta.persistence.criteria.JoinType;
26+
2527
/**
2628
* Configuration for subquery. Allows specifying custom root entity, joins and resolving property values from
2729
* search request either by property prefix or by a separate class holding all subquery restrictions.
@@ -51,4 +53,9 @@ public class SubqueryConfiguration {
5153
*/
5254
private String restrictionPropertyHolder;
5355

56+
/**
57+
* Default join type to be used for conditions. If not specified default is JoinType.INNER
58+
*/
59+
private JoinType defaultJoinType;
60+
5461
}

nrich-search/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,11 @@ association to `Role`. It is possible to map that connection by using `UserRole`
402402

403403
This configuration will search `UserRole` entity by all properties in `UserSearchRequest` that have a prefix `userRole`.
404404

405+
#### Default join type
406+
407+
[`SearchConfiguration`][search-configuration-url] supports specifying default join type for conditions and projections. If none is specified then inner join is used.
408+
This can be also customized for individual associations by specifying a `SearchJoin` with `joinType`. Specified `joinType` will then be used when building projections and conditions.
409+
405410
[//]: # (Reference links for readability)
406411

407412
[search-configuration-url]: ../nrich-search-api/src/main/java/net/croz/nrich/search/api/model/SearchConfiguration.java

nrich-search/src/main/java/net/croz/nrich/search/support/JpaQueryBuilder.java

+31-30
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
import jakarta.persistence.criteria.Subquery;
5353
import jakarta.persistence.metamodel.ManagedType;
5454
import java.util.ArrayList;
55-
import java.util.Arrays;
5655
import java.util.Collection;
5756
import java.util.Collections;
5857
import java.util.HashMap;
@@ -91,16 +90,16 @@ public <R, P> CriteriaQuery<P> buildQuery(R request, SearchConfiguration<T, P, R
9190
searchProjectionList = ProjectionListResolverUtil.resolveSearchProjectionList(resultClass);
9291
}
9392

94-
List<Selection<?>> projectionList = resolveQueryProjectionList(root, searchProjectionList, request);
93+
resolveAndApplyPredicateList(request, searchConfiguration, criteriaBuilder, root, query);
94+
95+
List<Selection<?>> projectionList = resolveQueryProjectionList(root, searchConfiguration.getDefaultJoinType(), searchProjectionList, request);
9596

9697
if (!CollectionUtils.isEmpty(projectionList)) {
9798
query.multiselect(projectionList);
9899
}
99100

100101
query.distinct(searchConfiguration.isDistinct());
101102

102-
resolveAndApplyPredicateList(request, searchConfiguration, criteriaBuilder, root, query);
103-
104103
if (sort != null && sort.isSorted()) {
105104
query.orderBy(QueryUtils.toOrders(sort, root, criteriaBuilder));
106105
}
@@ -193,14 +192,14 @@ private <R> void applyJoinsOrFetchesToQuery(boolean applyFetch, R request, Root<
193192
.forEach(searchJoin -> applyJoinOrJoinFetch(existingFetches, existingJoins, root, searchJoin, applyFetch));
194193
}
195194

196-
private <R> List<Selection<?>> resolveQueryProjectionList(Root<?> root, List<SearchProjection<R>> projectionList, R request) {
195+
private <R> List<Selection<?>> resolveQueryProjectionList(Root<?> root, JoinType defaultJoinType, List<SearchProjection<R>> projectionList, R request) {
197196
if (CollectionUtils.isEmpty(projectionList)) {
198197
return Collections.emptyList();
199198
}
200199

201200
return projectionList.stream()
202201
.filter(projection -> shouldApplyProjection(projection, request))
203-
.map(projection -> convertToSelectionExpression(root, projection))
202+
.map(projection -> convertToSelectionExpression(defaultJoinType, root, projection))
204203
.collect(Collectors.toList());
205204
}
206205

@@ -247,21 +246,21 @@ private <P, R> void resolveAndApplyPredicateList(R request, SearchConfiguration<
247246
}
248247

249248
private <P, R> List<Predicate> resolveQueryPredicateList(R request, SearchConfiguration<T, P, R> searchConfiguration, CriteriaBuilder criteriaBuilder, Root<?> root, CriteriaQuery<?> query) {
249+
JoinType defaultJoinType = searchConfiguration.getDefaultJoinType();
250250
Set<Restriction> restrictionList = new SearchDataParser(root.getModel(), request, SearchDataParserConfiguration.fromSearchConfiguration(searchConfiguration)).resolveRestrictionList();
251-
252251
Map<Boolean, List<Restriction>> restrictionsByType = restrictionList.stream().collect(Collectors.partitioningBy(Restriction::isPluralAttribute));
253252

254-
List<Predicate> mainQueryPredicateList = convertRestrictionListToPredicateList(restrictionsByType.get(false), root, criteriaBuilder);
253+
List<Predicate> mainQueryPredicateList = convertRestrictionListToPredicateList(restrictionsByType.get(false), root, criteriaBuilder, defaultJoinType);
255254

256255
List<Restriction> pluralRestrictionList = restrictionsByType.get(true);
257256
if (!CollectionUtils.isEmpty(pluralRestrictionList)) {
258257

259258
if (searchConfiguration.getPluralAssociationRestrictionType() == PluralAssociationRestrictionType.JOIN) {
260-
mainQueryPredicateList.addAll(convertRestrictionListToPredicateList(pluralRestrictionList, root, criteriaBuilder));
259+
mainQueryPredicateList.addAll(convertRestrictionListToPredicateList(pluralRestrictionList, root, criteriaBuilder, defaultJoinType));
261260
}
262261
else {
263262
SearchPropertyJoin searchPropertyJoin = resolveSearchPropertyJoin(root);
264-
Subquery<Integer> subquery = createSubqueryRestriction(root.getJavaType(), root, query, criteriaBuilder, pluralRestrictionList, searchPropertyJoin);
263+
Subquery<Integer> subquery = createSubqueryRestriction(root.getJavaType(), root, query, criteriaBuilder, pluralRestrictionList, searchPropertyJoin, defaultJoinType);
265264

266265
mainQueryPredicateList.add(criteriaBuilder.exists(subquery));
267266
}
@@ -274,39 +273,34 @@ private <P, R> List<Predicate> resolveQueryPredicateList(R request, SearchConfig
274273
return mainQueryPredicateList;
275274
}
276275

277-
private Subquery<Integer> createSubqueryRestriction(Class<?> subqueryEntityType, Root<?> parent, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder, Collection<Restriction> restrictionList, SearchPropertyJoin searchPropertyJoin) {
276+
private Subquery<Integer> createSubqueryRestriction(
277+
Class<?> subqueryEntityType, Root<?> parent, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder,
278+
Collection<Restriction> restrictionList, SearchPropertyJoin searchPropertyJoin, JoinType defaultJoinType
279+
) {
278280
Subquery<Integer> subquery = query.subquery(Integer.class);
279281
Root<?> subqueryRoot = subquery.from(subqueryEntityType);
280282

281283
subquery.select(criteriaBuilder.literal(1));
282284

283-
List<Predicate> subQueryPredicateList = convertRestrictionListToPredicateList(restrictionList, subqueryRoot, criteriaBuilder);
285+
List<Predicate> subQueryPredicateList = convertRestrictionListToPredicateList(restrictionList, subqueryRoot, criteriaBuilder, defaultJoinType);
284286

285-
Path<?> parentPath = PathResolvingUtil.calculateFullRestrictionPath(parent, PathResolvingUtil.convertToPathList(searchPropertyJoin.getParentProperty()));
286-
Path<?> subqueryPath = PathResolvingUtil.calculateFullRestrictionPath(subqueryRoot, PathResolvingUtil.convertToPathList(searchPropertyJoin.getChildProperty()));
287+
Path<?> parentPath = PathResolvingUtil.calculateFullPath(parent, defaultJoinType, PathResolvingUtil.convertToPathList(searchPropertyJoin.getParentProperty()));
288+
Path<?> subqueryPath = PathResolvingUtil.calculateFullPath(subqueryRoot, defaultJoinType, PathResolvingUtil.convertToPathList(searchPropertyJoin.getChildProperty()));
287289

288290
subQueryPredicateList.add(criteriaBuilder.equal(parentPath, subqueryPath));
289291

290292
return subquery.where(subQueryPredicateList.toArray(new Predicate[0]));
291293
}
292294

293-
private List<Predicate> convertRestrictionListToPredicateList(Collection<Restriction> restrictionList, Root<?> rootPath, CriteriaBuilder criteriaBuilder) {
295+
private List<Predicate> convertRestrictionListToPredicateList(Collection<Restriction> restrictionList, Root<?> rootPath, CriteriaBuilder criteriaBuilder, JoinType joinType) {
294296
List<Predicate> predicateList = new ArrayList<>();
295297

296298
restrictionList.stream().filter(Objects::nonNull).forEach(restriction -> {
297299
String[] pathList = PathResolvingUtil.convertToPathList(restriction.getPath());
298300

299-
if (restriction.isPluralAttribute()) {
300-
String[] pluralAttributePathList = Arrays.copyOfRange(pathList, 1, pathList.length);
301-
Path<?> fullPath = PathResolvingUtil.calculateFullRestrictionPath(rootPath.join(pathList[0]), pluralAttributePathList);
301+
Path<?> fullPath = PathResolvingUtil.calculateFullPath(rootPath, joinType, pathList);
302302

303-
predicateList.add(restriction.getSearchOperator().asPredicate(criteriaBuilder, fullPath, restriction.getValue()));
304-
}
305-
else {
306-
Path<?> fullPath = PathResolvingUtil.calculateFullRestrictionPath(rootPath, pathList);
307-
308-
predicateList.add(restriction.getSearchOperator().asPredicate(criteriaBuilder, fullPath, restriction.getValue()));
309-
}
303+
predicateList.add(restriction.getSearchOperator().asPredicate(criteriaBuilder, fullPath, restriction.getValue()));
310304
});
311305

312306
return predicateList;
@@ -324,10 +318,15 @@ private <R> List<Subquery<?>> resolveSubqueryList(R request, SearchPropertyConfi
324318
return Collections.emptyList();
325319
}
326320

327-
return subqueryConfigurationList.stream().map(subqueryConfiguration -> buildSubquery(request, searchPropertyConfiguration, root, query, criteriaBuilder, subqueryConfiguration)).filter(Objects::nonNull).collect(Collectors.toList());
321+
return subqueryConfigurationList.stream()
322+
.map(subqueryConfiguration -> buildSubquery(request, searchPropertyConfiguration, root, query, criteriaBuilder, subqueryConfiguration))
323+
.filter(Objects::nonNull)
324+
.collect(Collectors.toList());
328325
}
329326

330-
private <R> Subquery<Integer> buildSubquery(R request, SearchPropertyConfiguration searchPropertyConfiguration, Root<?> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder, SubqueryConfiguration subqueryConfiguration) {
327+
private <R> Subquery<Integer> buildSubquery(
328+
R request, SearchPropertyConfiguration searchPropertyConfiguration, Root<?> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder, SubqueryConfiguration subqueryConfiguration
329+
) {
331330
ManagedType<?> subqueryRoot = entityManager.getMetamodel().managedType(subqueryConfiguration.getRootEntity());
332331

333332
Set<Restriction> subqueryRestrictionList;
@@ -346,16 +345,18 @@ private <R> Subquery<Integer> buildSubquery(R request, SearchPropertyConfigurati
346345

347346
Subquery<Integer> subquery = null;
348347
if (!CollectionUtils.isEmpty(subqueryRestrictionList)) {
349-
subquery = createSubqueryRestriction(subqueryConfiguration.getRootEntity(), root, query, criteriaBuilder, subqueryRestrictionList, subqueryConfiguration.getJoinBy());
348+
subquery = createSubqueryRestriction(
349+
subqueryConfiguration.getRootEntity(), root, query, criteriaBuilder, subqueryRestrictionList, subqueryConfiguration.getJoinBy(), subqueryConfiguration.getDefaultJoinType()
350+
);
350351
}
351352

352353
return subquery;
353354
}
354355

355-
private <R> Selection<?> convertToSelectionExpression(Path<?> root, SearchProjection<R> projection) {
356+
private <R> Selection<?> convertToSelectionExpression(JoinType defaultJoinType, Root<?> root, SearchProjection<R> projection) {
356357
String[] pathList = PathResolvingUtil.convertToPathList(projection.getPath());
357358

358-
Path<?> path = PathResolvingUtil.calculateFullSelectionPath(root, pathList);
359+
Path<?> path = PathResolvingUtil.calculateFullPath(root, defaultJoinType, pathList);
359360

360361
String alias = projection.getAlias() == null ? pathList[pathList.length - 1] : projection.getAlias();
361362

nrich-search/src/main/java/net/croz/nrich/search/util/PathResolvingUtil.java

+32-15
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,24 @@
1818
package net.croz.nrich.search.util;
1919

2020
import jakarta.persistence.criteria.From;
21+
import jakarta.persistence.criteria.JoinType;
2122
import jakarta.persistence.criteria.Path;
23+
import jakarta.persistence.metamodel.Attribute;
24+
import jakarta.persistence.metamodel.EntityType;
2225
import java.util.ArrayList;
2326
import java.util.Arrays;
2427
import java.util.List;
28+
import java.util.Optional;
2529

2630
public final class PathResolvingUtil {
2731

2832
private static final String PATH_SEPARATOR = ".";
2933

3034
private static final String PATH_REGEX = "\\.";
3135

36+
// Hibernate will throw an exception if trying to find as an attribute but it can be used as an condition and as an projection
37+
private static final String CLASS_ATTRIBUTE_NAME = "class";
38+
3239
private PathResolvingUtil() {
3340
}
3441

@@ -52,28 +59,38 @@ public static String removeFirstPathElement(String[] path) {
5259
return String.join(PATH_SEPARATOR, Arrays.copyOfRange(path, 1, path.length));
5360
}
5461

55-
56-
public static Path<?> calculateFullRestrictionPath(Path<?> rootPath, String[] pathList) {
57-
return calculateFullPath(rootPath, pathList, false);
58-
}
59-
60-
public static Path<?> calculateFullSelectionPath(Path<?> rootPath, String[] pathList) {
61-
return calculateFullPath(rootPath, pathList, true);
62-
}
63-
64-
private static Path<?> calculateFullPath(Path<?> rootPath, String[] pathList, boolean isSelection) {
65-
int lastElementIndex = pathList.length - 1;
62+
public static Path<?> calculateFullPath(Path<?> rootPath, JoinType defaultJoinType, String[] pathList) {
63+
JoinType calculatedJoinPath = Optional.ofNullable(defaultJoinType).orElse(JoinType.INNER);
6664
Path<?> calculatedPath = rootPath;
6765

68-
for (int i = 0; i < pathList.length; i++) {
69-
if (isSelection || i == lastElementIndex) {
70-
calculatedPath = calculatedPath.get(pathList[i]);
66+
for (String currentPathSegment : pathList) {
67+
if (shouldJoinPath(calculatedPath, currentPathSegment)) {
68+
From<?, ?> from = (From<?, ?>) calculatedPath;
69+
70+
calculatedPath = from.getJoins().stream()
71+
.filter(join -> currentPathSegment.equals(join.getAttribute().getName()))
72+
.findFirst()
73+
.orElseGet(() -> from.join(currentPathSegment, calculatedJoinPath));
7174
}
7275
else {
73-
calculatedPath = ((From<?, ?>) calculatedPath).join(pathList[i]);
76+
calculatedPath = calculatedPath.get(currentPathSegment);
7477
}
7578
}
7679

7780
return calculatedPath;
7881
}
82+
83+
private static boolean shouldJoinPath(Path<?> calculatedPath, String currentPathSegment) {
84+
if (CLASS_ATTRIBUTE_NAME.equals(currentPathSegment)) {
85+
return false;
86+
}
87+
88+
if (calculatedPath.getModel() instanceof EntityType<?> entityType) {
89+
Attribute<?, ?> attribute = entityType.getAttribute(currentPathSegment);
90+
91+
return attribute.isCollection() || attribute.isAssociation();
92+
}
93+
94+
return false;
95+
}
7996
}

nrich-search/src/test/java/net/croz/nrich/search/repository/support/JpaQueryBuilderTest.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import jakarta.persistence.PersistenceContext;
4949
import jakarta.persistence.Tuple;
5050
import jakarta.persistence.criteria.CriteriaQuery;
51+
import jakarta.persistence.criteria.JoinType;
5152
import java.util.Arrays;
5253
import java.util.Collections;
5354
import java.util.HashMap;
@@ -795,21 +796,43 @@ void shouldSearchByNestedCollectionProperties() {
795796
void shouldNotAddAdditionalJoinsForSelection() {
796797
// given
797798
TestEntitySearchRequest request = TestEntitySearchRequest.builder()
799+
.nestedEntityNestedEntityName("name")
798800
.build();
799801

800802
SearchProjection<TestEntitySearchRequest> nameProjection = new SearchProjection<>("name");
801803
SearchProjection<TestEntitySearchRequest> nestedNameProjection = SearchProjection.<TestEntitySearchRequest>builder().path("nestedEntity.nestedEntityName").alias("nestedName").build();
802804

803805
SearchConfiguration<TestEntity, TestEntityDto, TestEntitySearchRequest> searchConfiguration = SearchConfiguration.<TestEntity, TestEntityDto, TestEntitySearchRequest>builder()
804806
.resultClass(TestEntityDto.class)
807+
.resolvePropertyMappingUsingPrefix(true)
805808
.projectionList(Arrays.asList(nameProjection, nestedNameProjection))
806809
.build();
807810

808811
// when
809812
CriteriaQuery<TestEntityDto> result = jpaQueryBuilder.buildQuery(request, searchConfiguration, Sort.unsorted());
810813

811814
// then
812-
assertThat(result.getRoots().iterator().next().getJoins()).isEmpty();
815+
assertThat(result.getRoots().iterator().next().getJoins()).hasSize(1);
816+
}
817+
818+
@Test
819+
void shouldJoinBySpecifiedDefaultJoinType() {
820+
// given
821+
TestEntitySearchRequest request = TestEntitySearchRequest.builder()
822+
.nestedEntityNestedEntityName("name")
823+
.build();
824+
825+
SearchConfiguration<TestEntity, TestEntity, TestEntitySearchRequest> searchConfiguration = SearchConfiguration.<TestEntity, TestEntity, TestEntitySearchRequest>builder()
826+
.defaultJoinType(JoinType.LEFT)
827+
.resolvePropertyMappingUsingPrefix(true)
828+
.build();
829+
830+
// when
831+
CriteriaQuery<TestEntity> result = jpaQueryBuilder.buildQuery(request, searchConfiguration, Sort.unsorted());
832+
833+
// then
834+
assertThat(result.getRoots().iterator().next().getJoins()).hasSize(1);
835+
assertThat(result.getRoots().iterator().next().getJoins()).extracting("joinType").containsOnly(JoinType.LEFT);
813836
}
814837

815838
@Test

0 commit comments

Comments
 (0)