@nhh Follow
Dienstag, 11. November 2020
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. ;)
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
}
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.