2

I want to build a simple endpoint that returns an Order object where I can search for this order by a single query parameter or a combination of several query parameters altogether. All of these query parameters are optional and the reason is that different people will access these orders based on the different Ids.

So for example: /order/items?itemId={itemId}&orderId={orderId}&deliveryId={deliveryId}&packetId={packetId}

@GetMapping(path = "/order/items", produces = "application/json")
public Order getOrders(@RequestParam Optional<String> itemId,
                              @RequestParam Optional<String> orderId,
                              @RequestParam Optional<String> deliveryId,
                              @RequestParam Optional<String> packetId) { }

I could of course also skip the Java Optional and use @RequestParam(required = false), but the question here is rather how do I escape the if-else or .isPresent() nightmare of checking whether the query params are null? Or is there an elegant way, depending on the constellation of params, to pass further to my service and Spring Data JPA repository.

lapadets
  • 1,067
  • 10
  • 38
  • I always just structure my SQL like this `WHERE (:myParam IS NULL OR TABLE.COLUMN = :myParam )` for optional parameters... that way you're query handles the null and you can get more out of that query.. so I would actually use `@RequestParam(required = false)` – RobOhRob Feb 20 '20 at 15:51
  • But a search endpoint should really return an array, not a single item – RobOhRob Feb 20 '20 at 15:51
  • 1
    Pass the nulls to the query and [let the query handle it](https://www.baeldung.com/spring-data-jpa-null-parameters). – Andrew S Feb 20 '20 at 15:52
  • Well in this case it should really be only a single Order if it exists. – lapadets Feb 20 '20 at 15:52
  • If it's a search endpoint (which it is), it should return an array of the results... also it's called `getOrders` which would also have people believe that it returns multiple.. – RobOhRob Feb 20 '20 at 15:54
  • Look at Artem Karp's answer – RobOhRob Feb 20 '20 at 15:54
  • Alright, I see what you mean and you are right about returning a list of Orders +1 – lapadets Feb 20 '20 at 15:57
  • To minimize the amount of `if`-`else`, you could use [query by example](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example). I put together an example in my [answer](https://stackoverflow.com/a/60323855/1426227). – cassiomolin Feb 20 '20 at 16:08
  • I have answered similar question here https://stackoverflow.com/questions/60243712/spring-data-jpa-named-query-ignoring-null-parameters/60246119#60246119 – Oleksii Valuiskyi Feb 20 '20 at 16:52
  • Answered many times: you can have all this done with zero code by utilising Spring Data support for QueryDSL plus the web Spring data web extensions - all documented in the reference. See here: https://stackoverflow.com/questions/46970689/multi-column-search-with-spring-jpa-specifications/46971053#46971053 – Alan Hay Feb 20 '20 at 18:22

2 Answers2

5

To minimize the amount of parameters in your method, you could define your query parameters as fields of a class:

@Data
public class SearchOrderCriteria {
    private String itemId;
    private String orderId;
    private String deliveryId;
    private String packetId;
}

Then receive an instance of such class in your controller method:

@GetMapping(path = "/order/items", produces = "application/json")
public ResponseEntity<OrderInfo> getOrder(SearchOrderCriteria searchCriteria) {
    OrderInfo order = orderService.findOrder(searchCriteria)
    return ResponseEntity.ok(order);
}

And, in your service, to avoid a bunch of if-else, you could use query by example:

public OrderInfo findOrder(SearchOrderCriteria searchCriteria) {

    OrderInfo order = new OrderInfo();
    order.setItemId(searchCriteria.getItemId());
    order.setOrderId(searchCriteria.getOrderId());
    order.setDeliveryId(searchCriteria.getDeliveryId());
    order.setPacketId(searchCriteria.getPacketId());

    Example<OrderInfo> example = Example.of(order);
    return orderRepository.findOne(example);
}
cassiomolin
  • 124,154
  • 35
  • 280
  • 359
  • I like this approach, but maybe I don't understand the whole picture (yet) :) I am still confused how, based on the query parameters in get endpoint to populate the fields in `SearchCriteria`? – lapadets Feb 20 '20 at 16:22
  • @lapadets Check the Spring Data documentation for more details on how [query by example](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example) works. – cassiomolin Feb 20 '20 at 16:23
  • 1
    Thanks! Your answer helped me a lot! – lapadets Feb 22 '20 at 20:05
0

My small suggestion is avoid to use too general API, for example you can split your mapping into several endpoints eg:/order/delivery/itemId/{itemId} and /order/delivery/deliveryId/{deliveryId} and /order/delivery/packetId/{packetId} and handle which you need to call on client side.

Artem Karp
  • 19
  • 2
  • Thanks for the suggestion - I would also generally do this this way, but the problem is that it needs to be one endpoint and it won't be called from multiple clients. – lapadets Feb 20 '20 at 16:25
  • In this case you can put all params in list filter for non-nulls like:List listWithoutNulls = list.stream() .filter(Objects::nonNull) .collect(Collectors.toList()) and than proceed on strategy pattern in order to choose what datasource needs to be called – Artem Karp Feb 20 '20 at 17:01