处理多个可选参数的Java Spring REST API

9

我目前正在为教学目的制作一个Spring Boot REST API项目,其中有一个包含22个列的相当大的表格已经被加载到了MySQL数据库中,并且我正在努力让用户有能力通过多个列(例如6个)过滤结果。

我目前正在扩展一个Repository,并初始化了一些方法,例如findByParam1和findByParam2和findByParam1OrderByParam2Desc等,并验证它们按照预期工作。我的问题是,最好的方法是允许用户在不编写大量条件/存储库方法变体的情况下利用所有6个可选的RequestParams。例如,我希望用户有能力击中url home/get-data/来获取所有结果,home/get-data?param1=xx来基于param1进行过滤,并且潜在地,home/get-data?param1=xx&param2=yy...&param6=zz来过滤所有可选参数。

供参考,这里是我的控制器的相关代码(大致上)。

@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();
}

到目前为止,我的问题是,按照我目前的处理方式,我将需要n!个方法来支持筛选功能,其中n等于我想要进行筛选的字段/列的数量。是否有更好的方法来处理这个问题,也许是在原地过滤存储库,以便我可以简单地在检查Map时过滤“原地”,看看用户确实填充了哪些过滤器?
编辑:所以我目前正在实施一个“hacky”解决方案,可能与J. West下面的评论有关。我假设用户将在请求URL中指定所有n个参数,如果他们没有(例如,他们指定p1-p4但不是p5和p6),我将生成SQL语句,只匹配非包含参数的LIKE'%'。它看起来像...
@Query("select u from User u where u.p1 = :p1 and u.p2 = :p2 ... and u.p6 = :p6") 
List<User> findWithComplicatedQueryAndSuch;

在控制器中,我会检测Map中的p5和p6是否为null,如果是,就将它们简单地更改为字符串“%”。我相信有一种更精确和直观的方法来做到这一点,尽管我还没有找到类似的东西。


嗯,这个有点难。我相信Spring支持类似这样的东西,只是我不知道确切的是什么;我现在正在查看文档,因为我很好奇。作为一个潜在的“hacky”解决方案,你可以返回整个对象,然后循环遍历并将你不想包含的字段设置为null。由于你正在映射一个对象,即使你按照正确的方式做了这个操作,查询中没有返回的字段也将为空。 - Trevor Bye
我不是100%确定你指的是什么意思,即,将我不想包含的字段设置为空,这样怎么帮助我避免编写n阶乘数量的存储库函数,比如findByP1和findByP1andP2和findByP1andP3等等?这可能类似于我现在正在实现的hacky方法(更新答案),其中我只是假设用户始终在过滤6个参数,并且如果他们不是,则生成SQL相当于LIKE '%'的内容,基本上不进行任何过滤...你是指将不想包含的字段设置为空吗? - purdoo
我觉得我可能误解了你的问题。看一下这个关于创建自定义JpaSpecificationExecutor的链接,它似乎是正确的方向。http://www.cubrid.org/wiki_ngrinder/entry/how-to-create-dynamic-queries-in-springdata - Trevor Bye
@fapple 这就是为什么人们使用QueryDsl http://www.querydsl.com/。在Spring应用程序中使用它非常普遍 - 而且易于集成 - https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/ - Don Cheadle
3个回答

8
您可以使用JpaSpecificationExecutor和自定义的Specification轻松实现此操作:https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/ 我建议使用包含所有可选参数的DTO替换HashMap,然后基于该DTO构建规范,当然您也可以保留HashMap并根据其构建规范。
基本上:
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
}

还有控制器:

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

服务:

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

代码库:

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

这只是一个从公司代码中改编的快速示例,你可以理解一下!


啊,看起来我走对了路。非常有趣,我以前从没遇到过这样的用例,但如果需要的话,我很高兴现在知道如何做了。 - Trevor Bye
这是一种非常简单和干净的解决方案,可支持大型查询修改,基本上没有任何开销,因为您可以直接从控制器使用“增强型”DTO(不再是DTO),并将其传递到存储库。 - dav1d

2

1
如果您的存储库类实现了JpaRepository接口,则可以使用Query By Example (QBE)技术更轻松地完成操作,因为该接口实现了QueryByExampleExecutor接口,该接口提供了一个以Example 对象作为参数的findAll方法。
对于您的情况,使用这种方法确实非常适用,因为您的实体具有许多字段,并且您希望用户能够获取与表示为实体字段子集的过滤器匹配的那些字段及其相应的值。
假设实体是User(就像在您的示例中一样),您想要创建端点以获取属性值等于指定值的用户。可以使用以下代码完成此操作:
实体类:
@Entity
public class User implements Serializable {
    private Long id;
    private String firstName;
    private String lastName;
    private Integer age;
    private String city;
    private String state;
    private String zipCode;
}

控制器类:
@Controller
public class UserController {
    private UserRepository repository;
    private UserController(UserRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public List<User> getMatchingUsers(@RequestBody User userFilter) {
        return repository.findAll(Example.of(userFilter));
    }
}

仓库类:

@Repository
public class UserRepository implements JpaRepository<User, Integer> {
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接