I had a requirement in one of my interview tests using Spring Boot where I had to create an endpoint that accepts a bunch of optional request params and then returns a list of cars based on these parameters like car model, license plate, engine type, manufacturer, driver, the company it was rent to etc etc. And car, driver and manufacturer are all separate entities.
I implement this functionality in JPARepository with a single JPQL query implementing LEFT JOINS and filter in where clause like licensePlate = licensePlateParameter OR licensePlatParameter is null etc.
The solution was working however the interviewer said the solution was scalable and maintainable. I should have implemented it using predicates. Can someone show me an example how could I implement such functionality using predicates that is easier to maintain? Some examples with code would be greatly appreciated.
I thought I was smart by catering both the optional parameters and found records with in a single call by checking if the parameter is null or not. Another question related to that I have in mind is it really a good practice to get all the records from DB and then filter it using predicates? Also how to we filter when we have multiple objects/entities involved, predicates can be created for a single type.
@Query("SELECT d FROM Driver d LEFT JOIN d.car c WHERE (d.name = :name OR :name is null) "
+ "and (c.licensePlate = :licensePlate OR :licensePlate is null) "
+ "and (c.rating = :rating OR :rating is null) " and so on
List<Driver> findByAttributes(@Param("name") String name,
@Param("licensePlate") String licensePlate,
@Param("rating") Integer rating,
and so on);
You can use the dynamic query in Spring JPA as like this:
public List<Employee> findByCriteria(String employeeName,String employeeRole){
return employeeDAO.findAll(new Specification<Employee>() {
@Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
if(employeeName!=null) {
predicates.add(criteriaBuilder.and(criteriaBuilder.like(root.get("employeeName"), "%"+employeeName+"%")));
}
if(employeeRole!=null){
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.get("employeeRole"), employeeRole)));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
});
}
For this you need to implement JpaSpecificationExecutor
in your repository.
Here is detail explanation for Spring JPA dynamic query
Spring has a wrapper around the JPA criteria API (that uses predicates) and is called the specification API.
What you can do when writing specifications is the following, write a specification for each criteria:
public static Specification<Car> withLicensePlate(String licensePlate) {
return (root, query, cb) -> licensePlate == null ? null : cb.equal(root.get("licensePlate"), licensePlate);
}
public static Specification<Car> withRating(String rating) {
return (root, query, cb) -> rating == null ? null : cb.equal(root.get("rating"), rating);
}
public static Specification<Car> withName(String name) {
return (root, query, cb) -> name == null ? null : cb.equal(root.get("name"), name);
}
it also allows you to write a join operation as well:
public static Specification<Car> withSeatType(String type) {
return (root, query, cb) -> {
return type == null ? null : cb.equal(root.join("interior", JoinType.LEFT).get("type"), type);
};
}
You can return null
within a criteria, which allows you to make these specifications "optional". After that, you can use Specifications.where()
to combine these criteria:
Specification<Car> spec = Specifications
.where(withLicensePlate(licensePlate))
.and(withRating(rating))
.and(withName(name))
.and(withSeatType(seatType));
If you write separate specifications like I did in this example, you can re-use them where necessary. Otherwise, you'll have to write operation-specific specifications, and the interviewer might not find that scalable either.
After writing the specifications, you have to extend your repository from the JpaSpecificationExecutor
interface and use the findAll(Specification)
method.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With