Skip to content

Commit 0835cc9

Browse files
committed
allow specifications with From instead of Root
See #2761
1 parent 961d825 commit 0835cc9

6 files changed

Lines changed: 165 additions & 28 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.springframework.data.jpa.domain;
2+
3+
import jakarta.persistence.criteria.*;
4+
5+
/**
6+
* Abstract base class for specifications which apply specifications on another joined entity.
7+
*
8+
* @param <T>
9+
* @param <S>
10+
*/
11+
public abstract class JoinSpecification<T, S> implements NestableSpecification<T> {
12+
13+
private NestableSpecification<S> specification;
14+
15+
protected JoinSpecification(Specification<S> specification) {
16+
if (!(specification instanceof NestableSpecification<S>)) {
17+
throw new IllegalArgumentException("specification is non-nestable");
18+
}
19+
this.specification = (NestableSpecification<S>) specification;
20+
}
21+
22+
@Override
23+
public final Predicate toPredicate(From<T, T> from, CriteriaQuery<?> query, CriteriaBuilder builder) {
24+
var joined = join(from);
25+
26+
return specification.toPredicate(joined, query, builder);
27+
}
28+
29+
/**
30+
* Join another entity.
31+
* @param from entity to join from
32+
* @return join
33+
*/
34+
protected abstract Join<S, S> join(From<T, T> from);
35+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2008-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain;
17+
18+
import jakarta.persistence.criteria.*;
19+
import org.springframework.lang.Nullable;
20+
21+
import java.io.Serializable;
22+
23+
/**
24+
* Specification in the sense of Domain Driven Design.
25+
* <p>
26+
* A {@link Specification} working with {@link From} instead of {@link Root}.
27+
*
28+
* @author Sven Meier
29+
*/
30+
@FunctionalInterface
31+
public interface NestableSpecification<T> extends Specification<T> {
32+
33+
@Nullable
34+
default Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
35+
return toPredicate((From)root, query, criteriaBuilder);
36+
}
37+
38+
/**
39+
* Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
40+
* {@link From} and {@link CriteriaBuilder}.
41+
*
42+
* @param from must not be {@literal null}.
43+
* @param query the criteria query.
44+
* @param criteriaBuilder must not be {@literal null}.
45+
* @return a {@link Predicate}, may be {@literal null}.
46+
*/
47+
@Nullable
48+
Predicate toPredicate(From<T, T> from, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
49+
50+
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.domain;
1717

18-
import jakarta.persistence.criteria.CriteriaBuilder;
19-
import jakarta.persistence.criteria.CriteriaQuery;
20-
import jakarta.persistence.criteria.Predicate;
21-
import jakarta.persistence.criteria.Root;
18+
import jakarta.persistence.criteria.*;
2219

2320
import java.io.Serial;
2421
import java.io.Serializable;
@@ -55,13 +52,21 @@ public interface Specification<T> extends Serializable {
5552
*/
5653
static <T> Specification<T> not(@Nullable Specification<T> spec) {
5754

58-
return spec == null //
59-
? (root, query, builder) -> null //
60-
: (root, query, builder) -> {
61-
62-
Predicate predicate = spec.toPredicate(root, query, builder);
63-
return predicate != null ? builder.not(predicate) : builder.disjunction();
64-
};
55+
if (spec == null) {
56+
return (root, query, builder) -> null;
57+
}
58+
59+
if (spec instanceof NestableSpecification nestable) {
60+
return (NestableSpecification<T>) (from, query, builder) -> {
61+
Predicate predicate = nestable.toPredicate(from, query, builder);
62+
return predicate != null ? builder.not(predicate) : builder.disjunction();
63+
};
64+
} else {
65+
return (root, query, builder) -> {
66+
Predicate predicate = spec.toPredicate(root, query, builder);
67+
return predicate != null ? builder.not(predicate) : builder.disjunction();
68+
};
69+
}
6570
}
6671

6772
/**
@@ -76,7 +81,7 @@ static <T> Specification<T> not(@Nullable Specification<T> spec) {
7681
*/
7782
@Deprecated(since = "3.5.0", forRemoval = true)
7883
static <T> Specification<T> where(@Nullable Specification<T> spec) {
79-
return spec == null ? (root, query, builder) -> null : spec;
84+
return spec == null ? (NestableSpecification<T>)(from, query, builder) -> null : spec;
8085
}
8186

8287
/**

spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717

1818
import java.io.Serializable;
1919

20-
import jakarta.persistence.criteria.CriteriaBuilder;
21-
import jakarta.persistence.criteria.CriteriaQuery;
22-
import jakarta.persistence.criteria.Predicate;
23-
import jakarta.persistence.criteria.Root;
20+
import jakarta.persistence.criteria.*;
2421

2522
import org.springframework.lang.Nullable;
2623

@@ -43,6 +40,20 @@ interface Combiner extends Serializable {
4340
static <T> Specification<T> composed(@Nullable Specification<T> lhs, @Nullable Specification<T> rhs,
4441
Combiner combiner) {
4542

43+
if (lhs instanceof NestableSpecification nlhs && rhs instanceof NestableSpecification nrhs) {
44+
return (NestableSpecification<T>)(from, query, builder) -> {
45+
46+
Predicate thisPredicate = SpecificationComposition.toPredicate(nlhs, from, query, builder);
47+
Predicate otherPredicate = toPredicate(nrhs, from, query, builder);
48+
49+
if (thisPredicate == null) {
50+
return otherPredicate;
51+
}
52+
53+
return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate);
54+
};
55+
}
56+
4657
return (root, query, builder) -> {
4758

4859
Predicate thisPredicate = toPredicate(lhs, root, query, builder);
@@ -61,4 +72,10 @@ private static <T> Predicate toPredicate(@Nullable Specification<T> specificatio
6172
CriteriaBuilder builder) {
6273
return specification == null ? null : specification.toPredicate(root, query, builder);
6374
}
75+
76+
@Nullable
77+
private static <T> Predicate toPredicate(@Nullable NestableSpecification<T> specification, From<T,T> from, @Nullable CriteriaQuery<?> query,
78+
CriteriaBuilder builder) {
79+
return specification == null ? null : specification.toPredicate(from, query, builder);
80+
}
6481
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package org.springframework.data.jpa.domain.sample;
1717

18+
import jakarta.persistence.criteria.From;
19+
import jakarta.persistence.criteria.Join;
20+
import org.springframework.data.jpa.domain.JoinSpecification;
21+
import org.springframework.data.jpa.domain.NestableSpecification;
1822
import org.springframework.data.jpa.domain.Specification;
1923

2024
/**
@@ -25,38 +29,48 @@
2529
*/
2630
public class UserSpecifications {
2731

28-
public static Specification<User> userHasFirstname(final String firstname) {
32+
public static NestableSpecification<User> userHasFirstname(final String firstname) {
2933

3034
return simplePropertySpec("firstname", firstname);
3135
}
3236

33-
public static Specification<User> userHasLastname(final String lastname) {
37+
public static NestableSpecification<User> userHasLastname(final String lastname) {
3438

3539
return simplePropertySpec("lastname", lastname);
3640
}
3741

38-
public static Specification<User> userHasFirstnameLike(final String expression) {
42+
public static NestableSpecification<User> userHasFirstnameLike(final String expression) {
3943

40-
return (root, query, cb) -> cb.like(root.get("firstname").as(String.class), String.format("%%%s%%", expression));
44+
return (from, query, cb) -> cb.like(from.get("firstname").as(String.class), String.format("%%%s%%", expression));
4145
}
4246

43-
public static Specification<User> userHasAgeLess(final Integer age) {
47+
public static NestableSpecification<User> userHasAgeLess(final Integer age) {
4448

45-
return (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), age);
49+
return (from, query, cb) -> cb.lessThan(from.get("age").as(Integer.class), age);
4650
}
4751

48-
public static Specification<User> userHasLastnameLikeWithSort(final String expression) {
52+
public static NestableSpecification<User> userHasLastnameLikeWithSort(final String expression) {
4953

50-
return (root, query, cb) -> {
54+
return (from, query, cb) -> {
5155

52-
query.orderBy(cb.asc(root.get("firstname")));
56+
query.orderBy(cb.asc(from.get("firstname")));
5357

54-
return cb.like(root.get("lastname").as(String.class), String.format("%%%s%%", expression));
58+
return cb.like(from.get("lastname").as(String.class), String.format("%%%s%%", expression));
5559
};
5660
}
5761

58-
private static <T> Specification<T> simplePropertySpec(final String property, final Object value) {
62+
private static <T> NestableSpecification<T> simplePropertySpec(final String property, final Object value) {
5963

60-
return (root, query, builder) -> builder.equal(root.get(property), value);
64+
return (from, query, builder) -> builder.equal(from.get(property), value);
65+
}
66+
67+
public static NestableSpecification<User> withManager(Specification<User> specification) {
68+
69+
return new JoinSpecification<>(specification) {
70+
@Override
71+
protected Join<User, User> join(From<User, User> from) {
72+
return from.join("manager");
73+
}
74+
};
6175
}
6276
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,22 @@ void executesSingleEntitySpecificationCorrectly() {
479479
assertThat(repository.findOne(userHasFirstname("Oliver"))).contains(firstUser);
480480
}
481481

482+
@Test
483+
void executesJoinedEntitySpecificationCorrectly() {
484+
485+
firstUser.setManager(secondUser);
486+
flushTestUsers();
487+
488+
assertThat(repository.findOne(
489+
withManager(
490+
allOf(
491+
userHasFirstname(secondUser.getFirstname()),
492+
userHasLastname(secondUser.getLastname())
493+
)
494+
))).contains(firstUser);
495+
}
496+
497+
482498
@Test
483499
void returnsNullIfNoEntityFoundForSingleEntitySpecification() {
484500

0 commit comments

Comments
 (0)