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

QueryDSL多参数过滤异常:面积/周长等条件无效求助

问题:QueryDSL多参数图形过滤逻辑失效

需求背景

需实现支持以下条件的图形数据过滤方法:

  • 图形类型
  • 面积范围(area from、area to)
  • 周长范围(circumference from、circumference to)
  • 创建时间范围(creation date from、to)
  • 创建人
  • 图形参数范围(from - to)

当前问题

面积、周长及图形参数过滤逻辑失效:

  • 传入无匹配参数时返回空JSON
  • 执行过滤操作时返回数据库全部图形数据

实体类代码

Shape.java

@Entity 
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) 
@DiscriminatorColumn(name="type", discriminatorType = DiscriminatorType.STRING) 
@Table(name = "shapes") 
@QueryEntity 
public class Shape implements Serializable{ 
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) 
private long id; 
@Column(insertable = false, updatable = false) 
private String type; 
@Version private int version; 
@ManyToOne @JoinColumn(name = "user_id") 
private User createdBy; 
private LocalDateTime createdAt; 
private LocalDateTime lastModifiedAt; 
private String lastModifiedBy; }

Square.java

@Entity
@DiscriminatorValue("SQUARE")
public class Square extends Shape {

    private double height;

    private double area;

    private double perimeter;

    @PrePersist
    @PreUpdate
    public void calculateAreaAndPerimeter() {
        area = height * height;
        perimeter = 4 * height;
    }
}

服务层代码

ShapeManagementService.java

@Service
public class ShapeManagementService implements IShapeManagementService {

    private final ShapeRepository shapeRepository;
    private final RoleRepository roleRepository;
    private final List<IShapeService> shapeServices;
    private final JPAQueryFactory queryFactory;

    private static final Logger logger = LoggerFactory.getLogger(ShapeManagementService.class);

    public ShapeManagementService(ShapeRepository shapeRepository, RoleRepository roleRepository, List<IShapeService> shapeServices, JPAQueryFactory queryFactory) {
        this.shapeRepository = shapeRepository;
        this.roleRepository = roleRepository;
        this.shapeServices = shapeServices;
        this.queryFactory = queryFactory;
    }

    @Transactional
    @Override
    public ShapeDto saveShape(ShapeRequestDto shapeRequestDto, String username) {
        IShapeService shapeService = shapeServices.stream()
                .filter(h -> h.getShape().equalsIgnoreCase(shapeRequestDto.getType()))
                .findFirst()
                .orElseThrow();
        return shapeService.save(shapeRequestDto, username);
    }

    @Override
    public List<ShapeDto> getFilteredShapes(String createdBy, String type, Double areaFrom, Double areaTo,
                                            Double perimeterFrom, Double perimeterTo, LocalDateTime createdAtFrom,
                                            LocalDateTime createdAtTo, Double heightFrom, Double heightTo) {
        QShape shape = QShape.shape;
        QSquare square = QSquare.square;
        QCircle circle = QCircle.circle;

        BooleanBuilder predicate = new BooleanBuilder();

        if (createdBy != null) {
            predicate.and(shape.createdBy.username.eq(createdBy));
        }

        if (type != null) {
            predicate.and(shape.type.eq(type));
        }

        if (areaFrom != null || areaTo != null || perimeterFrom != null || perimeterTo != null) {
            predicate.and(shape.instanceOf(Square.class));
        }

        if (areaFrom != null) {
            predicate.and(square.area.goe(areaFrom));
        }

        if (areaTo != null) {
            predicate.and(square.area.loe(areaTo));
        }

        if (perimeterFrom != null) {
            predicate.and(square.perimeter.goe(perimeterFrom));
        }

        if (perimeterTo != null) {
            predicate.and(square.perimeter.loe(perimeterTo));
        }

        if (heightFrom != null || heightTo != null) {
            predicate.andAnyOf(square.height.between(heightFrom, heightTo), circle.radius.between(heightFrom, heightTo));
        }

        if (createdAtFrom != null) {
            predicate.and(shape.createdAt.goe(createdAtFrom));
        }

        if (createdAtTo != null) {
            predicate.and(shape.createdAt.loe(createdAtTo));
        }

        JPAQuery<Shape> query = queryFactory.selectFrom(shape)
                .leftJoin(square).on(shape.instanceOf(Square.class))
                .leftJoin(circle).on(shape.instanceOf(Circle.class))
                .where(predicate)
                .groupBy(shape.id); // Grupowanie wyników po id figury

        List<Shape> shapes = query.fetch();

        return shapes.stream()
                .map(this::convertToDto)
                .collect(Collectors.toList());
    }
    public ShapeDto convertToDto(Shape shape) {
        ShapeDto shapeDto;
        if (shape instanceof Square) {
            shapeDto = convertSquareToDto((Square) shape);
//        } else if (shape instanceof Circle) {
//            shapeDto = convertCircleToDto((Circle) shape);
        } else {
            shapeDto = new ShapeDto();
        }

        shapeDto.setId(shape.getId());
        shapeDto.setType(shape.getType());
        shapeDto.setCreatedBy(shape.getCreatedBy().getUsername());
        shapeDto.setCreatedAt(shape.getCreatedAt());
        shapeDto.setLastModifiedAt(shape.getLastModifiedAt());
        shapeDto.setLastModifiedBy(shape.getLastModifiedBy());

        return shapeDto;
    }

    private SquareDto convertSquareToDto(Square square) {
        SquareDto squareDto = new SquareDto();
        squareDto.setHeight(square.getHeight());
        squareDto.setPerimeter(square.getPerimeter());
        squareDto.setArea(square.getArea());
        return squareDto;
    }
}

SquareService.java

@Component
public class SquareService implements IShapeService {

    private final SquareRepository squareRepository;
    private final UserRepository userRepository;
    private final ModelMapper modelMapper;
    private final SquareFactory squareFactory;
    private final ShapeRepository shapeRepository;

    public SquareService(SquareRepository squareRepository, UserRepository userRepository, ModelMapper modelMapper, SquareFactory squareFactory, ShapeRepository shapeRepository) {
        this.squareRepository = squareRepository;
        this.userRepository = userRepository;
        this.modelMapper = modelMapper;
        this.squareFactory = squareFactory;
        this.shapeRepository = shapeRepository;
    }

    @Transactional
    @Override
    public ShapeDto save(ShapeRequestDto shapeRequestDto, String username) {
        Square square = squareFactory.createSquare(shapeRequestDto, username);
        Square squareToSave = squareRepository.save(square);
        SquareDto squareDto = modelMapper.map(squareToSave, SquareDto.class);
        squareDto.getCreatedBy();
        squareDto.setCreatedBy(square.getCreatedBy().getUsername());
        squareDto.setArea(square.getArea());
        squareDto.setPerimeter(square.getPerimeter());
        return squareDto;
    }

    @Override
    public String getShape() {
        return "SQUARE";
    }
}

控制器代码

ShapeController.java

@RestController
@RequestMapping("/api/v1/shapes")
public class ShapeController {

    private final IShapeManagementService shapeManagementService;

    public ShapeController(IShapeManagementService shapeManagementService) {
        this.shapeManagementService = shapeManagementService;
    }
    
    @PreAuthorize("hasRole('ROLE_CREATOR')")
    @PostMapping
    public ResponseEntity<ShapeDto> addShape(@RequestBody ShapeRequestDto shapeRequestDto, Principal p) {
        ShapeDto shapeDto = shapeManagementService.saveShape(shapeRequestDto, p.getName());
        return ResponseEntity.status(HttpStatus.CREATED).body(shapeDto);

    }
    @GetMapping(value = "/parameters")
    public ResponseEntity<List<ShapeDto>> getShapes(@RequestParam(required = false) String createdBy,
                                                    @RequestParam(required = false) String type,
                                                    @RequestParam(required = false) Double areaFrom,
                                                    @RequestParam(required = false) Double areaTo,
                                                    @RequestParam(required = false) Double perimeterFrom,
                                                    @RequestParam(required = false) Double perimeterTo,
                                                    @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime createdAtFrom,
                                                    @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime createdAtTo,
                                                    @RequestParam(required = false) Double heightFrom,
                                                    @RequestParam(required = false) Double heightTo) {
        List<ShapeDto> filteredShapes = shapeManagementService.getFilteredShapes(
                createdBy, type, areaFrom, areaTo, perimeterFrom, perimeterTo, createdAtFrom, createdAtTo, heightFrom, heightTo);

        return ResponseEntity.status(HttpStatus.OK).body(filteredShapes);
    }
}

问题排查与修复方案

核心问题1:子类字段过滤未与类型判断绑定

当前代码中,面积、周长等子类字段的过滤条件直接加入全局predicate,但使用leftJoin时,非对应类型的实体(比如非Square的Shape)的子类字段会为null,导致过滤条件失效,这类实体被错误保留。

修复方式:将子类属性的过滤条件与instanceOf条件嵌套绑定,确保只有对应类型的实体才会应用字段过滤:

// 面积过滤示例
if (areaFrom != null || areaTo != null) {
    BooleanBuilder squareAreaPred = new BooleanBuilder();
    squareAreaPred.and(shape.instanceOf(Square.class));
    if (areaFrom != null) {
        squareAreaPred.and(square.area.goe(areaFrom));
    }
    if (areaTo != null) {
        squareAreaPred.and(square.area.loe(areaTo));
    }
    predicate.and(squareAreaPred);
}

核心问题2:图形参数过滤逻辑错误

当前andAnyOf的写法会导致非Square/Circle的实体跳过参数过滤,且Square实体不满足height条件时,因circle字段为null,条件被忽略,最终未过滤掉不符合的记录。

修复方式:为每个子类的参数过滤单独绑定类型判断,再用or组合:

if (heightFrom != null && heightTo != null) {
    BooleanBuilder squareHeightPred = new BooleanBuilder();
    squareHeightPred.and(shape.instanceOf(Square.class))
                   .and(square.height.between(heightFrom, heightTo));

    BooleanBuilder circleRadiusPred = new BooleanBuilder();
    circleRadiusPred.and(shape.instanceOf(Circle.class))
                   .and(circle.radius.between(heightFrom, heightTo));

    predicate.or(squareHeightPred).or(circleRadiusPred);
}

核心问题3:多余的分组操作

groupBy(shape.id)完全多余,每个Shape的id唯一,分组不会改变结果,反而影响查询性能,建议直接移除。

修复后的getFilteredShapes方法

@Override
public List<ShapeDto> getFilteredShapes(String createdBy, String type, Double areaFrom, Double areaTo,
                                        Double perimeterFrom, Double perimeterTo, LocalDateTime createdAtFrom,
                                        LocalDateTime createdAtTo, Double heightFrom, Double heightTo) {
    QShape shape = QShape.shape;
    QSquare square = QSquare.square;
    QCircle circle = QCircle.circle;

    BooleanBuilder predicate = new BooleanBuilder();

    if (createdBy != null) {
        predicate.and(shape.createdBy.username.eq(createdBy));
    }

    if (type != null) {
        predicate.and(shape.type.eq(type));
    }

    // 面积过滤:仅对Square生效
    if (areaFrom != null || areaTo != null) {
        BooleanBuilder squareAreaPred = new BooleanBuilder();
        squareAreaPred.and(shape.instanceOf(Square.class));
        if (areaFrom != null) {
            squareAreaPred.and(square.area.goe(areaFrom));
        }
        if (areaTo != null) {
            squareAreaPred.and(square.area.loe(areaTo));
        }
        predicate.and(squareAreaPred);
    }

    // 周长过滤:仅对Square生效
    if (perimeterFrom != null || perimeterTo != null) {
        BooleanBuilder squarePerimPred = new BooleanBuilder();
        squarePerimPred.and(shape.instanceOf(Square.class));
        if (perimeterFrom != null) {
            squarePerimPred.and(square.perimeter.goe(perimeterFrom));
        }
        if (perimeterTo != null) {
            squarePerimPred.and(square.perimeter.loe(perimeterTo));
        }
        predicate.and(squarePerimPred);
    }

    // 图形参数过滤:Square的height或Circle的radius
    if (heightFrom != null && heightTo != null) {
        BooleanBuilder squareHeightPred = new BooleanBuilder();
        squareHeightPred.and(shape.instanceOf(Square.class))
                       .and(square.height.between(heightFrom, heightTo));

        BooleanBuilder circleRadiusPred = new BooleanBuilder();
        circleRadiusPred.and(shape.instanceOf(Circle.class))
                       .and(circle.radius.between(heightFrom, heightTo));

        predicate.or(squareHeightPred).or(circleRadiusPred);
    }

    if (createdAtFrom != null) {
        predicate.and(shape.createdAt.goe(createdAtFrom));
    }

    if (createdAtTo != null) {
        predicate.and(shape.createdAt.loe(createdAtTo));
    }

    JPAQuery<Shape> query = queryFactory.selectFrom(shape)
            .leftJoin(square).on(shape.instanceOf(Square.class))
            .leftJoin(circle).on(shape.instanceOf(Circle.class))
            .where(predicate);

    List<Shape> shapes = query.fetch();

    return shapes.stream()
            .map(this::convertToDto)
            .collect(Collectors.toList());
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 06:37:01