SpecificationProjections.java
package expresspecs;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import org.jspecify.annotations.NonNull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor.SpecificationFluentQuery;
/**
* Factory methods for the projection function argument of
* {@code JpaSpecificationExecutor.findBy(Specification, Function)}. Where the rest of this library
* expresses the {@code WHERE} clause as a {@link org.springframework.data.jpa.domain.Specification},
* these helpers express how the matching rows are projected and shaped into results, replacing the
* noisy fluent-query lambda with a single call that reads as a phrase.
*/
public class SpecificationProjections {
/**
* Builds a query function that returns all matching entities projected as the given type,
* for use with {@code JpaSpecificationExecutor.findBy(Specification, Function)}.
* <p>
* The projection type may be a DTO (closed projection bound by constructor arguments) or an
* interface (open projection), as supported by Spring Data's
* {@link SpecificationFluentQuery#as(Class)}. This lets callers fetch only the columns they need
* instead of full entities, without writing a separate query method.
* </p>
* <b>Example:</b>
*
* <pre>{@code
* List<CustomerDTO> dtos = repository.findBy(isActive(), projectedAs(CustomerDTO.class));
* }</pre>
*
* @param <EntityType> the entity type the underlying query operates on.
* @param <ProjectedType> the type each result is projected into.
* @param projectedType the class to project each matching entity into.
* @return a function that, given the fluent query, returns the projected results as a {@link List}.
*/
public static <EntityType, ProjectedType> @NonNull Function<SpecificationFluentQuery<EntityType>, List<ProjectedType>> projectedAs(@NonNull Class<ProjectedType> projectedType) {
return q -> q.as(projectedType).all();
}
/**
* Builds a query function that returns a page of matching entities projected as the given type,
* for use with {@code JpaSpecificationExecutor.findBy(Specification, Function)}.
* <p>
* This is the paged and sorted counterpart of {@link #projectedAs(Class)}. The supplied
* {@link Pageable} controls page size, offset, and sort order, and the result carries the usual
* pagination metadata. The projection type may be a DTO or an interface, as supported by Spring
* Data's {@link SpecificationFluentQuery#as(Class)}.
* </p>
* <b>Example:</b>
*
* <pre>{@code
* Page<CustomerDTO> dtos = repository.findBy(isActive(), projectedAs(CustomerDTO.class, pageable));
* }</pre>
*
* @param <EntityType> the entity type the underlying query operates on.
* @param <ProjectedType> the type each result is projected into.
* @param projectedType the class to project each matching entity into.
* @param page the paging and sorting information to apply.
* @return a function that, given the fluent query, returns the projected results as a {@link Page}.
*/
public static <EntityType, ProjectedType> @NonNull Function<SpecificationFluentQuery<EntityType>, Page<ProjectedType>> projectedAs(@NonNull Class<ProjectedType> projectedType, @NonNull Pageable page) {
return q -> q.as(projectedType).page(page);
}
/**
* Builds a query function that returns the single matching entity projected as the given type,
* for use with {@code JpaSpecificationExecutor.findBy(Specification, Function)}.
* <p>
* The projection type may be a DTO or an interface, as supported by Spring Data's
* {@link SpecificationFluentQuery#as(Class)}. The result is empty when nothing matches; an
* exception is thrown when more than one row matches.
* </p>
* <b>Example:</b>
*
* <pre>{@code
* Optional<CustomerDTO> dto = repository.findBy(hasEmail(email), oneProjectedAs(CustomerDTO.class));
* }</pre>
*
* @param <EntityType> the entity type the underlying query operates on.
* @param <ProjectedType> the type the result is projected into.
* @param projectedType the class to project the matching entity into.
* @return a function that, given the fluent query, returns the single projected result as an {@link Optional}.
*/
public static <EntityType, ProjectedType> @NonNull Function<SpecificationFluentQuery<EntityType>, Optional<ProjectedType>> oneProjectedAs(@NonNull Class<ProjectedType> projectedType) {
return q -> q.as(projectedType).one();
}
/**
* Builds a query function that returns the first matching entity projected as the given type,
* for use with {@code JpaSpecificationExecutor.findBy(Specification, Function)}.
* <p>
* Unlike {@link #oneProjectedAs(Class)}, this tolerates multiple matches and returns the first,
* so it is typically paired with a sort. The projection type may be a DTO or an interface, as
* supported by Spring Data's {@link SpecificationFluentQuery#as(Class)}. The result is empty when
* nothing matches.
* </p>
* <b>Example:</b>
*
* <pre>{@code
* Optional<CustomerDTO> dto = repository.findBy(isActive(), firstProjectedAs(CustomerDTO.class));
* }</pre>
*
* @param <EntityType> the entity type the underlying query operates on.
* @param <ProjectedType> the type the result is projected into.
* @param projectedType the class to project the matching entity into.
* @return a function that, given the fluent query, returns the first projected result as an {@link Optional}.
*/
public static <EntityType, ProjectedType> @NonNull Function<SpecificationFluentQuery<EntityType>, Optional<ProjectedType>> firstProjectedAs(@NonNull Class<ProjectedType> projectedType) {
return q -> q.as(projectedType).first();
}
/**
* Builds a query function that returns the matching entities projected as the given type as a
* lazily-evaluated {@link Stream}, for use with
* {@code JpaSpecificationExecutor.findBy(Specification, Function)}.
* <p>
* The projection type may be a DTO or an interface, as supported by Spring Data's
* {@link SpecificationFluentQuery#as(Class)}. The returned stream is backed by an open result set
* and must be consumed within the surrounding transaction and closed when done, for example with
* a try-with-resources block.
* </p>
* <b>Example:</b>
*
* <pre>{@code
* try (Stream<CustomerDTO> dtos = repository.findBy(isActive(), streamProjectedAs(CustomerDTO.class))) {
* dtos.forEach(...);
* }
* }</pre>
*
* @param <EntityType> the entity type the underlying query operates on.
* @param <ProjectedType> the type each result is projected into.
* @param projectedType the class to project each matching entity into.
* @return a function that, given the fluent query, returns the projected results as a {@link Stream}.
*/
public static <EntityType, ProjectedType> @NonNull Function<SpecificationFluentQuery<EntityType>, Stream<ProjectedType>> streamProjectedAs(@NonNull Class<ProjectedType> projectedType) {
return q -> q.as(projectedType).stream();
}
}