You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化Spring Boot多条件商品搜索接口,轻松扩展筛选器?

商品搜索功能重构方案:实现可扩展的动态筛选

问题现状

当前实现中,为每种筛选组合编写了单独的Repository查询方法和Controller端点,导致代码冗余严重,新增price这类筛选条件时需要创建大量组合方法,扩展性极差。

最优重构方案:使用Spring Data JPA Specification实现动态查询

通过Specification可以动态拼接查询条件,无需为每种组合编写硬编码的查询方法,轻松支持新筛选条件的扩展。

步骤1:修改ProductRepository接口

让Repository继承JpaSpecificationExecutor,获得动态查询能力:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import com.vaibhavshrivastava.productbrowser.model.Product;
import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Integer>, JpaSpecificationExecutor<Product> {
    // 仅保留必要的单个查询:根据productCode查询单个商品
    Optional<Product> findByProductCode(Integer productCode);
}

步骤2:创建Specification构建工具类

编写工具类,根据传入的筛选参数动态构建查询条件:

import org.springframework.data.jpa.domain.Specification;
import com.vaibhavshrivastava.productbrowser.model.Product;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;

public class ProductSpecification {

    public static Specification<Product> buildSpecification(String name, String brand, Integer productCode, Integer minPrice, Integer maxPrice) {
        return (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            // 名称模糊匹配(参数非空时生效)
            if (name != null && !name.isEmpty()) {
                predicates.add(criteriaBuilder.like(root.get("name"), "%" + name + "%"));
            }
            
            // 品牌精确匹配(参数非空时生效)
            if (brand != null && !brand.isEmpty()) {
                predicates.add(criteriaBuilder.equal(root.get("brand"), brand));
            }
            
            // 商品编码精确匹配(参数非空时生效)
            if (productCode != null) {
                predicates.add(criteriaBuilder.equal(root.get("productCode"), productCode));
            }
            
            // 价格区间筛选(参数非空时生效)
            if (minPrice != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("price"), minPrice));
            }
            if (maxPrice != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("price"), maxPrice));
            }
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

步骤3:简化ProductController

只保留一个通用筛选端点,接收所有可选查询参数:

import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.vaibhavshrivastava.productbrowser.model.Product;
import com.vaibhavshrivastava.productbrowser.repository.ProductRepository;
import com.vaibhavshrivastava.productbrowser.specification.ProductSpecification;
import java.util.List;

@RestController
@CrossOrigin
@RequestMapping("/products")
public class ProductController {

    private final ProductRepository productRepository;

    // 构造注入替代@Autowired(符合Spring最佳实践)
    public ProductController(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // 通用筛选端点:支持name、brand、productCode、minPrice、maxPrice等可选参数
    @GetMapping
    public ResponseEntity<List<Product>> getProducts(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String brand,
            @RequestParam(required = false) Integer productCode,
            @RequestParam(required = false) Integer minPrice,
            @RequestParam(required = false) Integer maxPrice) {
        
        Specification<Product> spec = ProductSpecification.buildSpecification(name, brand, productCode, minPrice, maxPrice);
        List<Product> products = productRepository.findAll(spec);
        return new ResponseEntity<>(products, HttpStatus.OK);
    }

    // 保留根据productCode查询单个商品的端点
    @GetMapping("/{pid}/details")
    public ResponseEntity<Product> getProductDetails(@PathVariable("pid") Integer productCode) {
        Product selectedProduct = productRepository.findByProductCode(productCode).orElseThrow();
        return new ResponseEntity<>(selectedProduct, HttpStatus.OK);
    }
}

步骤4:前端Angular传参方式

前端通过查询参数传递筛选条件,示例:

  • 按品牌筛选:GET /products?brand=Apple
  • 名称模糊搜索+价格区间:GET /products?name=phone&minPrice=3000&maxPrice=8000
  • 多条件组合:GET /products?name=book&brand=Penguin

扩展新筛选条件的方式

如果需要新增description这类筛选,仅需两步:

  1. 在ProductSpecification.buildSpecification方法中添加对应条件:
    if (description != null && !description.isEmpty()) {
        predicates.add(criteriaBuilder.like(root.get("description"), "%" + description + "%"));
    }
    
  2. 在Controller的getProducts方法中添加对应的可选@RequestParam参数即可,无需修改其他代码。

其他可选方案

若项目复杂度较高,可选择QueryDSL,它提供类型安全的动态查询API,使用体验更流畅,但需要额外配置依赖和代码生成插件。

内容的提问来源于stack exchange,提问作者Innomight

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 05:55:47