This image shows a eagle

Implementing a dynamic search with spring boot


Weeks ago, we implemented a dynamic search in Ruby on Rails. This was easy to implement. Rails dynamic nature came in pretty handy. Given, that a user has several inputs, the query was straight forward to implement:

An example:

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 what we need to work with a spring object 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

It seems that we need a whole lot of java code to achieve the functionality provided by rails. But there are lot of examples and the only two things you need, are a implementation of Specification and the Repository implementing the JpaSpecificationExecutor. Then you have a flexible and extensible solution to a common problem.