SpecificationExtensions.java
package expresspecs;
import static expresspecs.BasicSpecifications.unrestricted;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.Predicate;
public class SpecificationExtensions {
/**
* Null-safe or() for Specifications that tolerates null values for either argument. If both arguments
* are null, {@link BasicSpecifications#unrestricted()} is returned.
*/
public static <T> Specification<T> safeOr(@Nullable Specification<T> existing, @Nullable Specification<T> additional) {
if (existing == null && additional == null) {
return unrestricted();
}
if (existing == null) {
return additional;
}
if (additional == null) {
return existing;
}
return existing.or(additional);
}
/**
* Null-safe and() for Specifications that tolerates null values for either argument. If both arguments
* are null, {@link BasicSpecifications#unrestricted()} is returned.
*/
public static <T> Specification<T> safeAnd(@Nullable Specification<T> existing, @Nullable Specification<T> additional) {
if (existing == null && additional == null) {
return unrestricted();
}
if (existing == null) {
return additional;
}
if (additional == null) {
return existing;
}
return existing.and(additional);
}
/**
* Wraps a {@link Specification} to ensure that results are distinct, but only
* when necessary.
* <p>
* This decorator performs two critical checks before applying a {@code DISTINCT} keyword:
* <ol>
* <li><b>Join Detection:</b> It only applies distinct logic if the query has
* actually performed a {@code JOIN}. This avoids the performance overhead of
* distinct sorting on simple single-table queries.</li>
* <li><b>Result Type Safety:</b> It ensures that {@code DISTINCT} is not
* applied to count queries (where the result type is {@link Long}), preventing
* potential JPA provider exceptions and incorrect metadata counts.</li>
* </ol>
* <p>
* <b>When to use:</b> Use this when building user-facing search queries that
* navigate {@code OneToMany} or {@code ManyToMany} relationships, where a
* single parent entity would otherwise appear multiple times in the result list due to
* multiple matching child records.
* </p>
* <b>Example:</b>
*
* <pre>{@code
* Specification<Customer> spec = smartDistinct(hasRecentOrder().and(isPremium()));
* List<Customer> results = repository.findAll(spec);
* }</pre>
*
* @param <T> The type of the entity the specification is checking.
* @param spec The underlying specification to execute.
* @return A new Specification that conditionally applies {@code DISTINCT}.
*/
public static <T> @NonNull Specification<T> smartDistinct(Specification<T> spec) {
return (root, query, cb) -> {
Predicate predicate = spec.toPredicate(root, query, cb);
if (!root.getJoins().isEmpty()) {
Class<?> resultType = query.getResultType();
if (resultType != Long.class && resultType != long.class) {
query.distinct(true);
}
}
return predicate;
};
}
}