In this tutorial, I will summarize the best way to filter information from our database and expose it through rest endpoints using JPA Specifications.

We’ll be using a dataset that contains some information about different car models, and we will build a small API that will allow us to search for car models given some input filters, such as price range, fuel type, brand etc…

What are JPA Specifications

JPA specifications allow us to perform queries by superimposing the necessary filters instead of generating different methods for each case to be evaluated. Let’s suppose we want to retrieve a list of cars with a specific fuel type and number of doors, and also retrieve cars filtered by brand and price. We will need to develop two different queries for this:

  • findAllByFuelTypeAndNumberOfDoors(FuelType fuelType, int numberOfDoors);
  • findAllByBrandAndPrice(String brand, BigDecimal price);

This might work, but the more complex the filters are, the more unmaintainable the code will be, since we will have to create a query for each specific case.

Using JPA specifications, our code will be much easier to maintain. The perfect place to use them is in the view layer where users select multiple filters to display data depending on date and price ranges, brand names, number of doors, engine, etc…

Let’s code it

For this tutorial, we are using just one table to store the car data from the dataset:

Entity

The dataset contains many other columns, but for simplicity we are just going to map a few ones, which I think are the most interesting: car brand, fuel type, number of doors and price range. Said this, our entity will look like this:

@Entity
@Table(name = "Car")
public class Car {

    @Id
    @Column(name = "id")
    private int id;

    @Column(name = "make")
    private String make;

    @Column(name = "fuel_type")
    @Enumerated(EnumType.STRING)
    private FuelType fuelType;

    @Column(name = "num_of_doors")
    private int numberOfDoors;

    @Column(name = "price")
    private BigDecimal price;
}

Repository

The next step is to add the data access layer, which is the Spring Repository. Since we are going to use specifications, it also needs to extend from JpaSpecificationExecutor:

@Repository
public interface CarRepository extends JpaRepository<Car, Integer>, JpaSpecificationExecutor<Car> {

    Page<Car> findAll(@Nullable Specification<Car> spec, @NonNull Pageable pageable);
}

Some important details here are the Specification, which will contain the dynamic query that we are going to build to filter the data, and the attribute Pageable, because we don’t know how many results can be returned by our database, so it’s a better practice to return a paged response to avoid performance issues.

Specification

Now it’s turn to develop the key part of the tutorial, the filter that will allow us to get a list of cars by:

  • Specific car brand
  • Fuel type: gasoline or diesel
  • Number of doors: 2 or 4
  • Price range

And the following code is all we need to implement to be able to do that:

public class CarSpec {

    public static final String MAKE = "make";
    public static final String FUEL_TYPE = "fuelType";
    public static final String NUMBER_OF_DOORS = "numberOfDoors";
    public static final String PRICE = "price";

    private CarSpec() {
        //empty
    }

    public static Specification<Car> filterBy(CarsFilter carsFilter) {
        return Specification
                .where(hasMake(carsFilter.make()))
                .and(hasFuelType(carsFilter.fuelType()))
                .and(hasNumberOfDoors(carsFilter.numberOfDoors()))
                .and(hasPriceGreaterThan(carsFilter.priceFrom()))
                .and(hasPriceLessThan(carsFilter.priceTo()));
    }

    private static Specification<Car> hasMake(String make) {
        return ((root, query, cb) -> make == null || make.isEmpty() ? cb.conjunction() : cb.equal(root.get(MAKE), make));
    }

    private static Specification<Car> hasFuelType(FuelType fuelType) {
        return (root, query, cb) -> fuelType == null ? cb.conjunction() : cb.equal(root.get(FUEL_TYPE), fuelType);
    }

    private static Specification<Car> hasNumberOfDoors(Integer numberOfDoors) {
        return (root, query, cb) -> numberOfDoors == null ? cb.conjunction() : cb.equal(root.get(NUMBER_OF_DOORS), numberOfDoors);
    }

    private static Specification<Car> hasPriceGreaterThan(BigDecimal priceFrom) {
        return (root, query, cb) -> priceFrom == null ? cb.conjunction() : cb.greaterThan(root.get(PRICE), priceFrom);
    }

    private static Specification<Car> hasPriceLessThan(BigDecimal priceTo) {
        return (root, query, cb) -> priceTo == null ? cb.conjunction() : cb.lessThan(root.get(PRICE), priceTo);
    }
}

Service

The only thing that we still need to do is to connect the data layer with the service, like this:

@Override
public CarsPageResponse<CarDto> searchCars(CarsFilter filter, int page, int size) {
    Specification<Car> spec = CarSpec.filterBy(filter);
    Page<Car> pageResult = carRepository.findAll(spec, PageRequest.of(page, size));
    return carMapper.toCarsPageResponse(pageResult);
}

Where the CarsFilter object will be instantiated by the controller through the query param used in our rest endpoint, let’s say:

GET /my-service/cars?brand=audi&numberOfDoors=4&priceFrom=15000&priceTo=20000

Summary

We have seen how easy is to develop filters that are exposed through REST endpoints and are converted into JPA Specifications that allow us to perform dynamic queries to our database.

You will find the complete Spring Boot application used for this post in my Github repository, so feel free to check it out and play around!

See you in the next post!