Provides a variety of components that reduce the overhead of composing and maintaining Specifications.
SpecificationFactory is used to generate Specification instances. It encapsulates anonymous Specification subclasses and provides null-safe handling.
SpecificationBuilder puts a fluent API on top of SpecificationFactory to compose compound Specifications with ease.
SpecificationUtil is used by SpecificationFactory to assist with null checking, wildcard detection and String conversions, etc.
The Specifications Annotation is available as a convenience, an alias of Spring's Component Annotation to mark Specification Beans as a particular kind of Bean.
SpecificationFactory and SpecificationUtil may be used independently, if desired. However, the intent is to use SpecificationBuilder exclusively, without being aware of either SpecificationFactory or SpecificationUtil, but it is not mandatory. Both SpecificationFactory and SpecificationUtil are declared with public access.
- Built-in null handling makes conditional query composition simple and easy – no need to wrap Specification conjunctions with a check for parameter state when parameters are optional.
- Built-in support for efficient eager fetching provides query optimization – an entire Aggregate can be loaded with one query instead of many.
- A fluent API encapsulating boilerplate code makes queries that are both strongly typed and easy to read – the risk of error is reduced while query logic is more coherent.
- A
@Specifications
Annotation complements Spring's@Controller
,@Service
and@Repository
Annotations – Specification Beans can be identified as a special kind of Bean by both developers and processes (like the execution of rules with ArchUnit, for example).
https://adoptium.net/temurin/releases/?version=17
https://hibernate.org/orm/tooling/
Add the Hibernate JPA Metamodel Generator to the Maven Compiler Plugin as an Annotation Processor:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.3.1.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
- Spring Boot Starter Data JPA 3.1.4
- Apache Commons Lang 3.13.0
Add this project's artifact to your project as a dependency:
<dependency>
<groupId>io.github.quinnandrews</groupId>
<artifactId>spring-data-specification-builder</artifactId>
<version>2.0.0</version>
</dependency>
(NOTE: This project's artifact is NOT yet available in Maven Central, but is available from GitHub Packages.)
Then extend your Repository Interfaces with JPASpecificationExecutor so that Specification methods are available:
import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.GuitarPedal;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface GuitarPedalRepository extends JpaRepository<GuitarPedal, Long>,
JpaSpecificationExecutor<GuitarPedal> {
}
Next, define your Specifications in a Specifications Bean (this is optional – you can also define Specification queries where they are used, in a Service, a Test, etc.)
import io.github.quinnandrews.spring.data.specification.annotations.Specifications;
import io.github.quinnandrews.spring.data.specification.builder.SpecificationBuilder;
import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.GuitarPedal;
import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.GuitarPedal_;
import org.springframework.data.jpa.domain.Specification;
@Specifications
public class GuitarPedalSpecifications {
public Specification<GuitarPedal> pedalsNotSoldWithValueGreaterThan(final Integer usedValue) {
return SpecificationBuilder.from(GuitarPedal.class)
.where().isNull(GuitarPedal_.dateSold)
.and().isGreaterThan(GuitarPedal_.usedValue, usedValue)
.toSpecification();
}
}
Finally, use your Specifications to execute queries:
import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.GuitarPedal;
import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.repository.GuitarPedalRepository;
import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.specifications.GuitarPedalSpecifications;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
public class GuitarPedalService {
private final GuitarPedalSpecifications guitarPedalSpecifications;
private final GuitarPedalRepository guitarPedalRepository;
public GuitarPedalService(final GuitarPedalSpecifications guitarPedalSpecifications,
final GuitarPedalRepository guitarPedalRepository) {
this.guitarPedalSpecifications = guitarPedalSpecifications;
this.guitarPedalRepository = guitarPedalRepository;
}
public List<GuitarPedal> getPedalsNotSoldWithValueGreaterThan(final Integer usedValue) {
return guitarPedalRepository.findAll(
guitarPedalSpecifications.pedalsNotSoldWithValueGreaterThan(usedValue),
Sort.by(GuitarPedal_.NAME)
);
}
}
The examples and tests use the Domain of Guitar Pedals (I'm a musician). It was simply more fun than using the Domains of TODOs or Employees.
GuitarPedalSpecifications contains the most comprehensive set of examples. It compares defining the same queries with and without the SpecificationBuilder, details gotchas and goes into more complex things like working with collections, for instance. Begin from the top and work your way down.
GuitarPedalSpecificationsIntegrationTest contains the Integrations Tests of the examples in GuitarPedalSpecifications. This may be useful to look at as well, or to run the examples yourself and see the sql output with your own eyes.
SpecificationBuilderIntegrationTest contains Integration Tests for the methods in SpecificationBuilder. Some of these tests cover cases that are not included in GuitarPedalSpecifications.
SpecificationBuilderTest, SpecificationFactoryTest and SpecificationUtilTest contain Unit Tests for the methods in their corresponding Classes. These may be useful to look at as well, in order to understand more about how things work under the hood, but it is not necessary.
- Build Specifications on Associations
Add versions ofwhere
methods that operate on Associations. It is expected the builder will need to maintain an instance variable containing Joins already created, so that they can be re-used during the build process if there is more than one Specification to apply to an Association. - Define JoinType of Associations
Add versions offetchOf()
that allow definition of JoinType. (Should it be applied towhere
methods on Associations as well?) - Add a
not()
Method in the Builder - Add a
clear()
Method in the Builder - Implement a SortBuilder to complement the SpecificationBuilder
Implement with the same sort of fluent-api and require Attributes instead of Strings for type safety.