Skip to content

Issue/2761 specifications with from #3919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 3.5.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.springframework.data.jpa.domain;

import jakarta.persistence.criteria.*;

/**
* Abstract base class for specifications which apply specifications on another joined entity.
*
* @param <T>
* @param <S>
*/
public abstract class JoinSpecification<T, S> implements NestableSpecification<T> {

private NestableSpecification<S> specification;

protected JoinSpecification(Specification<S> specification) {
if (!(specification instanceof NestableSpecification<S>)) {
throw new IllegalArgumentException("specification is non-nestable");
}
this.specification = (NestableSpecification<S>) specification;
}

@Override
public final Predicate toPredicate(From<T, T> from, CriteriaQuery<?> query, CriteriaBuilder builder) {
var joined = join(from);

return specification.toPredicate(joined, query, builder);
}

/**
* Join another entity.
* @param from entity to join from
* @return join
*/
protected abstract Join<S, S> join(From<T, T> from);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2008-2025 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
*
* https://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 org.springframework.data.jpa.domain;

import jakarta.persistence.criteria.*;
import org.springframework.lang.Nullable;

import java.io.Serializable;

/**
* Specification in the sense of Domain Driven Design.
* <p>
* A {@link Specification} working with {@link From} instead of {@link Root}.
*
* @author Sven Meier
*/
@FunctionalInterface
public interface NestableSpecification<T> extends Specification<T> {

@Nullable
default Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return toPredicate((From)root, query, criteriaBuilder);
}

/**
* Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
* {@link From} and {@link CriteriaBuilder}.
*
* @param from must not be {@literal null}.
* @param query the criteria query.
* @param criteriaBuilder must not be {@literal null}.
* @return a {@link Predicate}, may be {@literal null}.
*/
@Nullable
Predicate toPredicate(From<T, T> from, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
*/
package org.springframework.data.jpa.domain;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.*;

import java.io.Serial;
import java.io.Serializable;
Expand Down Expand Up @@ -55,13 +52,21 @@ public interface Specification<T> extends Serializable {
*/
static <T> Specification<T> not(@Nullable Specification<T> spec) {

return spec == null //
? (root, query, builder) -> null //
: (root, query, builder) -> {

Predicate predicate = spec.toPredicate(root, query, builder);
return predicate != null ? builder.not(predicate) : builder.disjunction();
};
if (spec == null) {
return (root, query, builder) -> null;
}

if (spec instanceof NestableSpecification nestable) {
return (NestableSpecification<T>) (from, query, builder) -> {
Predicate predicate = nestable.toPredicate(from, query, builder);
return predicate != null ? builder.not(predicate) : builder.disjunction();
};
} else {
return (root, query, builder) -> {
Predicate predicate = spec.toPredicate(root, query, builder);
return predicate != null ? builder.not(predicate) : builder.disjunction();
};
}
}

/**
Expand All @@ -76,7 +81,7 @@ static <T> Specification<T> not(@Nullable Specification<T> spec) {
*/
@Deprecated(since = "3.5.0", forRemoval = true)
static <T> Specification<T> where(@Nullable Specification<T> spec) {
return spec == null ? (root, query, builder) -> null : spec;
return spec == null ? (NestableSpecification<T>)(from, query, builder) -> null : spec;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@

import java.io.Serializable;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.*;

import org.springframework.lang.Nullable;

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

if (lhs instanceof NestableSpecification nlhs && rhs instanceof NestableSpecification nrhs) {
return (NestableSpecification<T>)(from, query, builder) -> {

Predicate thisPredicate = SpecificationComposition.toPredicate(nlhs, from, query, builder);
Predicate otherPredicate = toPredicate(nrhs, from, query, builder);

if (thisPredicate == null) {
return otherPredicate;
}

return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate);
};
}

return (root, query, builder) -> {

Predicate thisPredicate = toPredicate(lhs, root, query, builder);
Expand All @@ -61,4 +72,10 @@ private static <T> Predicate toPredicate(@Nullable Specification<T> specificatio
CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(root, query, builder);
}

@Nullable
private static <T> Predicate toPredicate(@Nullable NestableSpecification<T> specification, From<T,T> from, @Nullable CriteriaQuery<?> query,
CriteriaBuilder builder) {
return specification == null ? null : specification.toPredicate(from, query, builder);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package org.springframework.data.jpa.domain.sample;

import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Join;
import org.springframework.data.jpa.domain.JoinSpecification;
import org.springframework.data.jpa.domain.NestableSpecification;
import org.springframework.data.jpa.domain.Specification;

/**
Expand All @@ -25,38 +29,48 @@
*/
public class UserSpecifications {

public static Specification<User> userHasFirstname(final String firstname) {
public static NestableSpecification<User> userHasFirstname(final String firstname) {

return simplePropertySpec("firstname", firstname);
}

public static Specification<User> userHasLastname(final String lastname) {
public static NestableSpecification<User> userHasLastname(final String lastname) {

return simplePropertySpec("lastname", lastname);
}

public static Specification<User> userHasFirstnameLike(final String expression) {
public static NestableSpecification<User> userHasFirstnameLike(final String expression) {

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

public static Specification<User> userHasAgeLess(final Integer age) {
public static NestableSpecification<User> userHasAgeLess(final Integer age) {

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

public static Specification<User> userHasLastnameLikeWithSort(final String expression) {
public static NestableSpecification<User> userHasLastnameLikeWithSort(final String expression) {

return (root, query, cb) -> {
return (from, query, cb) -> {

query.orderBy(cb.asc(root.get("firstname")));
query.orderBy(cb.asc(from.get("firstname")));

return cb.like(root.get("lastname").as(String.class), String.format("%%%s%%", expression));
return cb.like(from.get("lastname").as(String.class), String.format("%%%s%%", expression));
};
}

private static <T> Specification<T> simplePropertySpec(final String property, final Object value) {
private static <T> NestableSpecification<T> simplePropertySpec(final String property, final Object value) {

return (root, query, builder) -> builder.equal(root.get(property), value);
return (from, query, builder) -> builder.equal(from.get(property), value);
}

public static NestableSpecification<User> withManager(Specification<User> specification) {

return new JoinSpecification<>(specification) {
@Override
protected Join<User, User> join(From<User, User> from) {
return from.join("manager");
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,22 @@ void executesSingleEntitySpecificationCorrectly() {
assertThat(repository.findOne(userHasFirstname("Oliver"))).contains(firstUser);
}

@Test
void executesJoinedEntitySpecificationCorrectly() {

firstUser.setManager(secondUser);
flushTestUsers();

assertThat(repository.findOne(
withManager(
allOf(
userHasFirstname(secondUser.getFirstname()),
userHasLastname(secondUser.getLastname())
)
))).contains(firstUser);
}


@Test
void returnsNullIfNoEntityFoundForSingleEntitySpecification() {

Expand Down