Spring Boot优雅实现Question资源多条件过滤Rest Controller方案
嘿,这个场景我太熟悉了——一开始加几个@RequestParam还能应付,过滤条件一多,Controller方法就会变得像个参数垃圾堆,空值判断也能写得让人头大。别担心,有几个优雅的方案能彻底解决这个问题:
第一步:用DTO封装所有查询参数
首先,把所有可能的过滤条件封装成一个查询DTO,这样Controller层就不用写一堆参数了,以后新增过滤条件只需要在DTO里加字段就行,完全不用修改Controller方法。
比如创建一个QuestionFilterDTO:
import lombok.Data; // 用Lombok简化getter/setter,不想用的话手动生成也可以 @Data public class QuestionFilterDTO { private String area; private String skill; private String proposedBy; private String difficultyLevel; private String state; // 你能想到的所有过滤字段都可以加在这里 }
然后修改你的Controller,用@ModelAttribute自动绑定请求参数到DTO:
@RestController @RequestMapping("/questions") public class QuestionController { @Autowired private QuestionService questionService; @GetMapping public List<Question> getFilteredQuestions(@ModelAttribute QuestionFilterDTO filter) { // 直接把DTO丢给业务层处理,Controller层彻底清爽 return questionService.filterQuestions(filter); } }
这样不管是/questions?area=technical还是/questions?skill=algorithm&proposedby=agrawalo,所有参数都会自动映射到DTO的对应字段里,空参数会被设为null,不用在Controller里做任何判断。
第二步:用动态查询处理多条件过滤
接下来解决业务层的动态过滤问题——总不能写一堆if-else拼接SQL吧?这里推荐两种主流方案:
方案1:Spring Data JPA Specification
如果你用的是Spring Data JPA,可以用Specification来动态构建查询条件,完全避免硬写SQL或者JPQL。
首先让你的Repository继承JpaSpecificationExecutor:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface QuestionRepository extends JpaRepository<Question, Long>, JpaSpecificationExecutor<Question> { }
然后在业务层实现过滤逻辑,根据DTO里的非空参数构建查询条件:
import org.springframework.data.jpa.domain.Specification; import javax.persistence.criteria.*; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; @Service public class QuestionService { @Autowired private QuestionRepository questionRepository; public List<Question> filterQuestions(QuestionFilterDTO filter) { Specification<Question> spec = (root, query, cb) -> { List<Predicate> predicates = new ArrayList<>(); // 过滤area(忽略大小写) if (StringUtils.isNotBlank(filter.getArea())) { predicates.add(cb.equal(cb.lower(root.get("area")), filter.getArea().toLowerCase())); } // 过滤skill(关联集合查询,忽略大小写) if (StringUtils.isNotBlank(filter.getSkill())) { Join<Question, Skill> skillJoin = root.join("skills"); predicates.add(cb.equal(cb.lower(skillJoin.get("skillName")), filter.getSkill().toLowerCase())); } // 过滤proposedBy(忽略大小写) if (StringUtils.isNotBlank(filter.getProposedBy())) { predicates.add(cb.equal(cb.lower(root.get("proposedBy")), filter.getProposedBy().toLowerCase())); } // 其他过滤条件可以照猫画虎添加 if (StringUtils.isNotBlank(filter.getDifficultyLevel())) { predicates.add(cb.equal(cb.lower(root.get("difficultyLevel")), filter.getDifficultyLevel().toLowerCase())); } // 把所有条件用AND连接起来 return cb.and(predicates.toArray(new Predicate[0])); }; return questionRepository.findAll(spec); } }
方案2:QueryDSL(更简洁的类型安全查询)
如果你觉得Specification的代码有点啰嗦,可以用QueryDSL——它提供了类型安全的查询API,代码可读性更高,还能避免写错字段名的低级错误。
首先需要引入QueryDSL的依赖(以Maven为例):
<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <scope>provided</scope> </dependency>
然后添加Maven插件自动生成Q类(对应你的实体类,比如QQuestion):
<build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins> </build>
然后让你的Repository继承QuerydslPredicateExecutor:
import org.springframework.data.jpa.repository.JpaRepository; import com.querydsl.core.types.Predicate; public interface QuestionRepository extends JpaRepository<Question, Long>, QuerydslPredicateExecutor<Question> { }
最后业务层的代码就变得非常简洁了:
import com.querydsl.core.BooleanBuilder; import org.apache.commons.lang3.StringUtils; @Service public class QuestionService { @Autowired private QuestionRepository questionRepository; public List<Question> filterQuestions(QuestionFilterDTO filter) { QQuestion qQuestion = QQuestion.question; BooleanBuilder builder = new BooleanBuilder(); // 过滤area if (StringUtils.isNotBlank(filter.getArea())) { builder.and(qQuestion.area.toLowerCase().eq(filter.getArea().toLowerCase())); } // 过滤skill if (StringUtils.isNotBlank(filter.getSkill())) { builder.and(qQuestion.skills.any().skillName.toLowerCase().eq(filter.getSkill().toLowerCase())); } // 过滤proposedBy if (StringUtils.isNotBlank(filter.getProposedBy())) { builder.and(qQuestion.proposedBy.toLowerCase().eq(filter.getProposedBy().toLowerCase())); } // 其他条件同理添加 return (List<Question>) questionRepository.findAll(builder); } }
总结一下
这一套组合拳下来,你的代码会变得非常清爽:
- Controller层只负责接收参数,不用关心具体过滤逻辑;
- DTO封装所有查询条件,扩展性极强,新增条件不用改Controller;
- 动态查询方案(Specification/QueryDSL)避免了大量
if-else拼接SQL的冗余代码,还能支持任意组合的过滤条件。
内容的提问来源于stack exchange,提问作者ThinkGeek

