If you are using JPA, I recommend using Spring Data JPA and the Specification API. When I originally looked at Spring Boot and Spring Data, the only real approach I saw promoted was Spring Data Rest which was too much magic for my taste.
I personally like to have Controllers invoke methods on services (reusable business logic layer that's basically the Facade pattern). These services eventually call Spring Data repositories.
Consider a User object
public class User{
String firstName;
String lastName;
}
Let's say we have a UserController handling our REST requests for the User resource. This controller converts a Request with UserSearchCriteria to a Spring Data Page<User>
import org.springframework.data.domain.Page;
public class UserController{
@Autowired
UserService userService;
@RequestMapping(path = "/users", method = RequestMethod.GET)
Page<User> getAll(HttpServletRequest request, UserSearchCriteria searchCriteria){
return userService.findAllUsers(searchCriteria);
}
}
The UserSearchCriteria are the types of search params your client would pass as query params like GET /users?firstName=mista&lastName=henry which get automapped to UserSearchCriteria fields.
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.PageRequest;
public class UserSearchCriteria {
UserSearchCriteria(){
super();
sort = "lastName";
}
Integer size;
Integer page;
String sort;
String sortDir;
String firstName;
String lastName;
Sort buildSort(){
return new Sort(new Sort.Order(Sort.Direction.ASC, sort).ignoreCase());
}
PageRequest toPageRequest(){
if(size == null){
size = Integer.MAX_VALUE; // may or may not be a good idea for your usecase
}
return new PageRequest(page, size, buildSort());
}
}
PageRequest and Sort are part of the Spring Data project. In my projects, I let the sorting and paging existing in an AbstractSearchCriteria object for easier reuse which all of my search criterias extend, but it's simpler to demonstrate as above.
In the UserService layer, I will delegate to my repository (I can also check access requirements, set defaults, etc.
import org.springframework.data.domain.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Transactional
public class UserService{
@Autowired
UserRepository userRepository;
Page<User> findAllUsers(UserSearchCriteria userSearchCriteria){
if(userSearchCriteria == null){
userSearchCriteria = new UserSearchCriteria();
}
return userRepository.findAll(UserSearchSpecification.findByCriteria(userSearchCriteria), userSearchCriteria.toPageRequest());
}
}
The UserSearchSpecification uses the Specification API to dynamically add searches to your JPA call, which I find cleaner than using the Criteria API directly.
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
public class UserSearchSpecification{
public static Specification<User> findByCriteria(final UserSearchCriteria searchCriteria){
return new Specification<User>() {
@Override
Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
// if firstName in criteria, do an uppercase prefix match
if(searchCriteria.firstName != null){
predicates.add(
cb.like(
cb.upper(root.get("firstName")),
"%" + searchCriteria.lastName.toUpperCase()
)
);
}
// if lastName in criteria, do an uppercase prefix match
if(searchCriteria.lastName != null){
predicates.add(
cb.like(
cb.upper(root.get("lastName")),
"%" + searchCriteria.lastName.toUpperCase()
)
);
}
if(predicates.size() > 0){
return cb.and(predicates.toArray());
}else{
return null;
}
}
}
}
}
This allows you to easily inspect the passed in params from the rest call (properties on the UserSearchSpecification) and dynamically build up the restrictions for the database call. At the end, I chose to and together each predicate, but you can do whatever you want here. You can also check for equality instead of like, greater/less than, etc.
Lastly, notice that this specification is passed to the UserRepository in the UserService.findAllUsers method. This is a Spring Data repository:
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface UserRepository extends PagingAndSortingRepository<User, Long>, JpaSpecificationExecutor<User>{
}
In Spring Data, you often only need to extend certain repositories in an interface like here. Spring Data can handle the rest under the hood. It's important to note the extension of PagingAndSortingRepository which handles the paging and sorting aspect as well as the JpaSpecificationExecutor which takes Specification objects and generates the correct query.
This may seem like a lot of code, but this has scaled really well for me as the codebase grows. Once you have the specification in place, you can easily add new fields to your SearchCriteria object and add new restrictions in the Specification by inspecting the new fields and creating Predicate.