12

I want to build a spring controller that can handle multiple, optional sort queries. According to spring.io spec these queries should be formatted thus

&sort=name,desc&sort=code,desc

As discussed by EduardoFernandes

I know this can be done with one instance of sort with value to be sorted and the direction give separately as per Gregg but that doesn't match the Spring spec or handle multiple sort values.

I'm not sure how to turn multiple queries in the spring spec format into a Sort that I can pass to my PageRequest and then on to my repository. Also I would like the ability to make these optional and if possible, it would be great if I could use @Anotation based config if defaults are necessary to achieve this as per Rasheed Amir (@SortDefault)

Here is the basics of what I'm working with..

Domain

    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class Subject {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private String code; 
    ...

Repository

    public interface SubjectRepository extends JpaRepository<Subject, Long> {
    }

Service

    @Override
    public Page<SubjectDTO> listSubjectsPageble(PageRequest pageableRequest) {
        return subjectRepository.findAll(pageableRequest)
                .map(subjectMapper::subjectToSubjectDTO);
    }

Controller

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('LECTURER')")
    public Page<SubjectDTO> getSubjects(
            @RequestParam("page") int page,
            @RequestParam("size") int size,
            @RequestParam("sort") String sort
    
    ) {
    
        return subjectService.listSubjectsPageble(PageRequest.of(page, size, new Sort(sort)));
    }

So here in the controller I don't know how to deal with\populate the Sort from the RequestParam at all, according to Ralph I should be able to use something like the below to get multiple values from one param, but I don't know how to then pass that to a Sort.

I know a Sort can take more than one parameter but only one sort direction. And then of coarse I would like to make them optional.

    @RequestParam MultiValueMap<String, String> params

Please help, I'm still quite a noob :) Thanks

EDIT

I solved some of my issues thanks to a post by Dullimeister But the approach feels a little messy and still doesn't handle multiple sort parameters. Does anyone know of better approach or is this the way to go?

    @GetMapping
        @ResponseStatus(HttpStatus.OK)
        @PreAuthorize("hasRole('LECTURER')")
        public Page<SubjectDTO> getSubjects(
                @RequestParam(value = "page", defaultValue = "0", required = false) int page,
                @RequestParam(value = "size", defaultValue = "10", required = false) int size,
                @RequestParam(value = "sort", defaultValue = "name,ASC", required = false) String sortBy
    
        ) {
            String[] sort = sortBy.split(",");
            String evalSort = sort[0];
            String sortDirection = sort[1];
            Sort.Direction evalDirection = replaceOrderStringThroughDirection(sortDirection);
            Sort sortOrderIgnoreCase = Sort.by(new Sort.Order(evalDirection,evalSort).ignoreCase());
    
            return subjectService.listSubjectsPageble(PageRequest.of(page, size, sortOrderIgnoreCase));
        }
    
        private Sort.Direction replaceOrderStringThroughDirection(String sortDirection) {
            if (sortDirection.equalsIgnoreCase("DESC")){
                return Sort.Direction.DESC;
            } else {
                return Sort.Direction.ASC;
            }
        }

Final Solution Thanks everyone, this is what I ended up with. Not sure if its the perfect way but it works :) I had to replace the comma with a semi-colon in the end as the FormattingConversionService was automatically parsing a single sort param to a string instead of an Sting[]

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('LECTURER')")
    public Page<SubjectDTO> getSubjects(
            @RequestParam(value = "page", defaultValue = "0", required = false) int page,
            @RequestParam(value = "size", defaultValue = "10", required = false) int size,
            @RequestParam(value = "sort", defaultValue = "name;ASC", required = false) String[] sortBy) {
        Sort allSorts = Sort.by(
            Arrays.stream(sortBy)
                    .map(sort -> sort.split(";", 2))
                    .map(array ->
                            new Sort.Order(replaceOrderStringThroughDirection(array[1]),array[0]).ignoreCase()
                    ).collect(Collectors.toList())
        );

        return subjectService.listSubjectsPageble(PageRequest.of(page, size, allSorts));
    }

    private Sort.Direction replaceOrderStringThroughDirection(String sortDirection) {
        if (sortDirection.equalsIgnoreCase("DESC")){
            return Sort.Direction.DESC;
        } else {
            return Sort.Direction.ASC;
        }
    }
MrE
  • 19,584
  • 12
  • 87
  • 105

1 Answers1

12

Why don't you use Pageable in your controller ?

Pageable can handle many sort queries, each of them will be stored in orders list. Moreover, any of pageable parameters aren't required. When you don't pass them in url, pageable will contains default values (page = 0, size = 20). You can change default values by using @PageableDefault annotation.

GET .../test?sort=name,desc&sort=code,desc enter image description here

  • Thanks for the reply..Isnt the only way to implement a Pageable is to use a PageRequest? As far as I can tell there inst a constructor for more than one sort? ([Pageable](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Pageable.html) ) – Rinus van Heerden Feb 20 '19 at 17:20
  • Yes, pageable always contains only one sort, but in that sort you can have many orders. – Mateusz Jaszewski Feb 20 '19 at 17:58
  • Ok thanks, One issue now is when I try to pull multiple sort Strings from the @RequestParam I want it to be a collection in order to stream against it. However the format $sort="id,asc" is being split by its comma into separate rows in the array ie `@RequestParam(value = "sort", defaultValue = "name,ASC", required = false) List sortBy` – Rinus van Heerden Feb 20 '19 at 18:13
  • I want to stream here to build all the "Orders" for the sort like thus `Sort allSorts = Sort.by( sortBy.stream() .map(sort -> sort.split(",", 2)) .map(array -> new Sort.Order(replaceOrderStringThroughDirection(array[0]),array[1]).ignoreCase() ).collect(Collectors.toList()) );` – Rinus van Heerden Feb 20 '19 at 18:14
  • But sortBy is giving sortBy0='"id" and sortBy1="ASC" instead of sortBy0= "id,asc" – Rinus van Heerden Feb 20 '19 at 18:29