Bild mit mehreren Lupen um eine Suchfunktion zu skizzieren

Implementing a dynamic search in spring data with specifications and predicates


Several weeks ago, we implemented a very dynamic search in Ruby on Rails. This was a relatively easy requirement for us, where rails dynamic nature came in pretty handy. Given, that the user had several inputs, which can be changed through a backend, the query was pretty straight forward to implement with rails:

It boils down to:

class CustomerController < ApplicationController

    # maps to POST /api/customers/search
    def search
        params = { name: "Niklas", address: "@home" } # This is a example for action params
        customers = Customer.where(params)
        render json: customers.to_json, status: :ok
    end

end

With this code snipped we can handle searches extremely flexible. One of the reasons i really like working with rails.

The opposite is shown when working with spring boot at the first look:

We have to implement each "find" action per hand

package com.example.springboot.repository;

import com.example.springboot.model.Customer;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.Optional;

public interface CustomerRepository extends PagingAndSortingRepository {
    public Optional findByName(final String name);
    public Optional findByPrice(final Float name);
}

We can implement a method which handles multiple search values, but again we have to do that at compile time.

package com.example.springboot.repository;

import com.example.springboot.model.Customer;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.Optional;

public interface CustomerRepository extends PagingAndSortingRepository {
    public Optional findByNameOrPrice(final String name, final Float name);
}

So, how can we achieve a flexible, but clean searching solution in spring boot? I'm gonna show you now. ;)

Guide

The first thing we have to do, is to let our repository implement another interface:

package com.example.springboot.repository;

import com.example.springboot.model.Customer;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.Optional;

public interface CustomerRepository extends PagingAndSortingRepository, JpaSpecificationExecutor {
    // Omitting the findByVALUE methods here...
}

JpaSpecificationExecutor prepares everything we need to work with a spring object principle called Specification. A Specification is the spring way to define a Object responsible for describing Predicates. That means we can have multiple Predicates chaing with and or or. You may want to check out this old blog article of Oliver Drotbohm: Advanced Spring Data JPA - Specifications and Querydsl

A Specification for a "example-search" could look like the following: ("example-search" refers to a search, where we can find similar items based on a example entity)

package com.example.springboot.specification;

import com.example.springboot.model.Customer;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

public class CustomerSpecification implements Specification {

    private final Customer customer;

    public CustomerSpecification(Customer customer) {
        this.customer = customer;
    }

    @Override
    public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        // It would also be possible to chain the ".equal" methods with ".and" or ".notNull"
        return criteriaBuilder.or(
            criteriaBuilder.equal(root.get("id"), this.customer.getId()),
            criteriaBuilder.equal(root.get("name"), this.customer.getIsoCode())
        );
    }

}

As you can see, the criteriaBuilder let us very dynamically choose properties of an entity we want to expose as searchable. The only thing we now have to do is to pass the CustomerSpecification to a findAll.

package com.example.springboot.specification;

import com.example.springboot.model.Customer;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

@Controller
@RequestMapping("/api/customers")
public class CustomerController {

    @PostMapping("/search")
    public ResponseEntity> searchCustomers(@Valid @RequestBody CustomerSearchRequest request) {

        CustomerSpecification spec = new CustomerSpecification(request.toCustomer());

        List customers = StreamSupport
                .stream(this.customerService.findAll(spec).spliterator(), false)
                .map(CustomerResponse::new)
                .collect(Collectors.toList());

        return new ResponseEntity<>(customers, HttpStatus.OK);
    }

}

With this piece of code, we are able to send an example customer to our backend, which searches for the example. You might wonder what the CustomerSearchRequest and CustomerResponse look like:

package com.example.springboot.dto;

import com.example.springboot.model.Customer;

public class CustomerSearchRequest {

    private Long id;
    private String name;
    private String address;

    public Customer toCustomer() {
        Customer customer = new Customer();

        customer.setName(this.name);
        customer.setAddress(this.address);

        if(this.id != null) {
            customer.setId(this.id);
        }

        return customer;
    }

    // Getter and Setters are omitted
}
package com.example.springboot.model.;

import com.example.springboot.model.Customer;

public class CustomerResponse {

    private final Long id;
    private final String name;

    private final String address;

    public CustomerResponse(Customer customer) {
        this.id = customer.getId();
        this.name = customer.getName();
        this.address = customer.getAddress();
    }

    // Getter and Setters are omitted
}

Summary

All in all it seems that we need a whole lot of java code to achieve the functionality provided by rails. But you should keep in mind that i showed you a lot of examples and the only two things you need, are a implementation of the Interface Specification and the Repository implementing the JpaSpecificationExecutor. Now you have a flexible and extensible solution to a common problem. (at leas a common problem to me... ;)


Similar Posts