2

For the sake of simplicity lets assume I have a Document object with seven fields (but imagine that it can have many more). This object looks something like this:

@Getter
@Setter
public class Document {
    private String fileName;
    private String fileType;
    private String createdBy;
    private Date createdAt;
    private Date lastModifiedAt;
    private List<String> modifiers;
    private Long timesModified;
}

I want to create an endpoint which can receive any number of @RequestParam and returns a List<Document> of all the documents which match the given query. For example: return all documents with fileType == doc, which were created between createdAt == 01/01/2021 && createdAt 31/01/2021, modified timesModified == 5 times and modifiers.contains("Alex"). The reason for this is that I want to allow the user to query for documents depending on combination of fields the user wants. Originally to handle this we created the endpoint like so:

@GetMapping(value = {RestApi.LIST})
public ResponseEntity<List<Document>> getDocuments (@RequestParam Map<String, Object> optionalFilters) {
    List<Document> documents = documentService.getListOfDocuments(optionalFilters);
    if (documents != null) {
        return new ResponseEntity<>(documents, HttpStatus.OK);
    }
    return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}

The problem with this is that because we use optionalFilters as Map<String, Object> this requires us to perform a lot of casting in our code and overall makes our code very tedious and cumbersome because we have to iterate through the whole map and create a custom query depending the fields that were passed. In order to try and improve this I created an OptionalFilters object:

@Getter
@Setter
@NoArgsConstructor
public class OptionalFilters {
    private String fileName;
    private String fileType;
    private String createdBy;
    private Date createdAt;
    private Date lastModifiedAt;
    private List<String> modifiers;
    private Long timesModified;
}

And modified the endpoint to this:

@GetMapping(value = {RestApi.LIST})
public ResponseEntity<List<Document>> getDocuments (@Valid OptionalFilters optionalFilters) {
    List<Document> documents = documentService.getListOfDocuments(optionalFilters);
    if (documents != null) {
        return new ResponseEntity<>(documents, HttpStatus.OK);
    }
    return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}

However, although that this simplifies the way we receive the parameters and extract the values from them, we still need to iterate through all the parameters and create a custom query. Is there some way to elevate and take advantage of Spring-Data (or any other solution) so that I don't have to create a custom query depending on each query param that is passed through? I am using Solr as the repository if this may be any help.

dabadie
  • 501
  • 1
  • 5
  • 17
  • If you are using Solr you can create dynamic queries as well. – Gurkan İlleez Jan 31 '21 at 09:59
  • https://lucidworks.com/post/spring-data-solr-tutorial-dynamic-queries/ – Gurkan İlleez Jan 31 '21 at 10:03
  • try this: https://stackoverflow.com/questions/52354315/spring-data-jpa-generate-dynamic-query – Roie Beck Jan 31 '21 at 10:16
  • @Gurkanİlleez thanks for your reply, this is what I'm currently doing today. The problem with this is that I need to create a custom query depending on the parameters that are sent in which is what I want to avoid. I want to create a single query which I can reuse regardless of the parameters that are sent in. – dabadie Jan 31 '21 at 15:01
  • @RoieBeck Thanks I'll check it out and update with the result – dabadie Jan 31 '21 at 15:02

1 Answers1

1

Using Query by Example is one the most simple option but it has its limitations. Excerpt from the above link:

  1. Limitations
    Like all things, the Query by Example API has some limitations. For instance:
  • Nesting and grouping statements are not supported, for example: (firstName = ?0 and lastName = ?1) or seatNumber = ?2
  • String matching only includes exact, case-insensitive, starts, ends, contains, and regex All types other than String are exact-match only

Query by Example is suitable choice if your filtering is never too complicated. But when restirictions like above hit the fan of your CPU cooler the choice is to use Specifications to construct queries.

One big difference is also that Using Query by Example you need to explicitly populate the example by its getters and setters. With specification you can make it in a generic way (with Java generics) using just use field names

In your case you could just pass the map to generic method and create filtering by just looping and adding by and (note that the link's example has static stuff mostly but it has not to be, you just need field name/criterion -pair to loop it in a generic way)

With specifications you can do anything that can be done with Query by Example and almost anything else also. The overhead to get familiar with specifications might be bigger but the advantage using specifications will be rewarding.

In a nutshell:

Spring interface Specification is based on JPA CriteriaQuery and for each you need only to implement one method:

Predicate toPredicate (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder);

Repository interfaces needs just to extend JpaSpecificationExecutor<YourClass> When you have a set of predicates, you can - for example -

repository.findAll(Specification.where(spec1).and(spec2));

It might seem complicated or difficult at start but it is not that at all. The greatest advantage with Specification is that you can do almost anything programmatically instead of manipulating JPQL queries or so.

pirho
  • 11,565
  • 12
  • 43
  • 70