Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Spring REST API Handling Many Optional Parameters

I'm currently messing around with a Spring Boot REST API project for instructional purposes. I have a rather large table with 22 columns loaded into a MySQL database and am trying to give the user the ability to filter the results by multiple columns (let's say 6 for the purposes of this example).

I am currently extending a Repository and have initialized methods such as findByParam1 and findByParam2 and findByParam1OrderByParam2Desc and etc. and have verified that they are working as intended. My question to you guys is the best way to approach allowing the user the ability to leverage all 6 optional RequestParams without writing a ridiculous amount of conditionals/repository method variants. For example, I want to give the user the ability to hit url home/get-data/ to get all results, home/get-data?param1=xx to filter based on param1, and potentially, home/get-data?param1=xx&param2=yy...&param6=zz to filter on all the optional parameters.

For reference, here is what the relevant chunk of my controller looks like (roughly).

@RequestMapping(value = "/get-data", method = RequestMethod.GET)
public List<SomeEntity> getData(@RequestParam Map<String, String> params) {
    String p1 = params.get("param1");
    if(p1 != null) {
        return this.someRepository.findByParam1(p1);
    }
    return this.someRepository.findAll();
}

My issue so far is that the way I am proceeding about this means that I will basically need n! amount of methods in my repository to support this functionality with n equalling the amount of fields/columns I want to filter on. Is there a better way to approach handling this, perhaps where I am filtering the repository 'in-place' so I can simply filter 'in-place' as I check the Map to see what filters the user did indeed populate?

EDIT: So I'm currently implementing a 'hacky' solution that might be related to J. West's comment below. I assume that the user will be specifying all n parameters in the request URL and if they do not (for example, they specify p1-p4 but not p5 and p6) I generate SQL that just matches the statement to LIKE '%' for the non-included params. It would look something like...

@Query("select u from User u where u.p1 = :p1 and u.p2 = :p2 ... and u.p6 = :p6") 
List<User> findWithComplicatedQueryAndSuch;

and in the Controller, I would detect if p5 and p6 were null in the Map and if so, simply change them to the String '%'. I'm sure there is a more precise and intuitive way to do this, although I haven't been able to find anything of the sort yet.

like image 440
purdoo Avatar asked Dec 09 '16 21:12

purdoo


People also ask

How do I send optional parameters in REST API?

We can make a route parameter optional by adding “?” to it. The only gimmick is while declaring a route parameter as optional, we must specify a consequent default value for it: [HttpGet("GetById/{id?}")]

How do I make optional arguments in spring boot?

Using an Optional Parameter Type. Here, Spring creates the Optional<Integer> instance, optionalArticleId, to hold the value of id. If id is present, optionalArticleId will wrap its value, otherwise, optionalArticleId will wrap a null value.

Can RequestParam be optional?

Method parameters annotated with @RequestParam are required by default.

Is Spring GOOD FOR REST API?

Advantages of using Spring Boot A few benefits of using Spring Boot for your REST APIs include: No requirement for complex XML configurations. Embedded Tomcat server to run Spring Boot applications. An auto-configuration feature by Spring Boot that configures your application automatically for certain dependencies.


2 Answers

You can do this easily with a JpaSpecificationExecutor and a custom Specification: https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

I would replace the HashMap with a DTO containing all optional get params, then build the specifications based on that DTO, obviously you can also keep the HashMap and build the specification based on it.

Basically:

public class VehicleFilter implements Specification<Vehicle>
{
    private String art;
    private String userId;
    private String vehicle;
    private String identifier;

    @Override
    public Predicate toPredicate(Root<Vehicle> root, CriteriaQuery<?> query, CriteriaBuilder cb)
    {
        ArrayList<Predicate> predicates = new ArrayList<>();

        if (StringUtils.isNotBlank(art))
        {
            predicates.add(cb.equal(root.get("art"), art));
        }
        if (StringUtils.isNotBlank(userId))
        {
            predicates.add(cb.equal(root.get("userId"), userId));
        }
        if (StringUtils.isNotBlank(vehicle))
        {
            predicates.add(cb.equal(root.get("vehicle"), vehicle));
        }
        if (StringUtils.isNotBlank(identifier))
        {
            predicates.add(cb.equal(root.get("identifier"), fab));
        }

        return predicates.size() <= 0 ? null : cb.and(predicates.toArray(new Predicate[predicates.size()]));
    }

// getter & setter
}

And the controller:

@RequestMapping(value = "/{ticket}/count", method = RequestMethod.GET)
public long getItemsCount(
    @PathVariable String ticket,
    VehicleFilter filter,
    HttpServletRequest request
) throws Exception
{
    return vehicleService.getCount(filter);
}

Service:

@Override
public long getCount(VehicleFilter filter)
{
    return vehicleRepository.count(filter);
}

Repository:

@Repository
public interface VehicleRepository extends JpaRepository<Vehicle, Integer>, JpaSpecificationExecutor<Vehicle>
{
}

Just a quick example adapted from company code, you get the idea!

like image 83
dav1d Avatar answered Oct 13 '22 09:10

dav1d


Another solution with less coding would be to use QueryDsl integration with Spring MVC.

By using this approach all your request parameters will be automatically resolved to one of your domain properties and appended to your query.

For reference check the documentation https://spring.io/blog/2015/09/04/what-s-new-in-spring-data-release-gosling#querydsl-web-support and the example project https://github.com/spring-projects/spring-data-examples/tree/master/web/querydsl

like image 21
Alex Ciocan Avatar answered Oct 13 '22 09:10

Alex Ciocan