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
.