OptaPlanner约束流Collector实现指导:同位素生产约束开发
问题:OptaPlanner约束流中自定义ConstraintCollector实现难题
我查阅OptaPlanner约束流相关文档后,仍无法理解ConstraintCollector的输入输出要求,无法写出可在constraint stream的groupBy()中使用的ConstraintCollector。
我的业务场景是将Employee分配至Shift:
Employee关联Tracer;Tracer关联Isotope,且包含batchesPerProduction属性;Isotope关联Contract,后者限制该同位素每日最大生产次数;- 每次同位素生产可产出多批次
Tracer,每个Shift消耗1批次Tracer。
需要实现的约束逻辑:按日期统计各Tracer的批次总量,计算所需同位素生产次数(生产次数 = 当日Shift数量 / Tracer.getBatchesPerProduction(),向上取整),确保总生产次数不超过合同日上限。
我梳理的实现步骤:
- 遍历所有
Tracer; - 关联对应
Employee的Shift; - 按
Tracer、日期分组,聚合计算同位素生产次数; - 按
Isotope、日期分组,聚合生产次数总和。
我尝试了BiConstraintCollector和TriConstraintCollector两种写法均未成功,也不清楚如何在groupBy()中调用。同时对Java和OptaPlanner的Stream接口、ConstraintCollectors.compose方法理解不足。
业务模型代码
@PlanningEntity public class Shift { @PlanningVariable private Employee employee = null; private Isotope isotope; private OffsetDateTime startDateTime; private OffsetDateTime endDateTime; } public class Employee { private Tracer tracer; } public class Tracer { private Isotope isotope; private Long batchesPerProduction; } public class Isotope { private Contract contract; } public class Contract { private Integer maximumProductionsPerDay; }
尝试的ConstraintCollector代码
public static <A,B> BiConstraintCollector<Tracer, LocalDate, ?, Long> batchesToProductions(ToLongFunction<A,B> groupValueMapping) { return compose(countLongBi(groupValueMapping), (tracer, day, countLongBi) -> { return (long) Math.ceil(countLongBi / tracer.getBatchesPerProduction()); }); }
public static <A,B,C> TriConstraintCollector<Tracer, LocalDate, Long, ?, Long> batchesToProductions(ToLongFunction<A,B,C> groupValueMapping) { return compose((tracer, day, shiftsCount) -> { return (long) Math.ceil(shiftsCount / tracer.getBatchesPerProduction()); }); }
尝试的约束流代码
Constraint dailyProductionsMustNotExceedContractMaximum(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Tracer.class) .join(Shift.class, equal(Function.identity(), Shift::getTracerForEmployee)) .groupBy( (tracer,shift) -> tracer, (tracer,shift) -> shift.getStartDateTime().toLocalDate(), (tracer,shift) -> batchesToProductions(tracer,shift)) .groupBy( (tracer, day, productions) -> tracer.getIsotope(), (tracer, day, productions) -> day, (tracer, day, productions) -> sum(productions) ) .filter((isotope, day, productions) -> productions > isotope.getContract().getMaximumShiftsPerDay()) .penalizeConfigurableLong(CONSTRAINT_DAILY_PRODUCTIONS_MUST_NOT_EXCEED_CONTRACT_MAXIMUM, (isotope, day, productions) -> productions - isotope.getContract().getMaximumShiftsPerDay()); }
解决方案
1. 核心思路:优先组合内置Collector
ConstraintCollector的本质是分组后的聚合逻辑,大部分场景无需自定义,通过组合OptaPlanner内置的Collector即可实现需求,避免复杂的类型参数处理。
2. 修正后的约束流实现
调整遍历起点为规划实体Shift,关联关系更直接,逻辑更清晰:
Constraint dailyProductionsMustNotExceedContractMaximum(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) // 关联到Employee,再关联到对应的Tracer .join(Employee.class, equal(Shift::getEmployee, Function.identity())) .join(Tracer.class, equal(Employee::getTracer, Function.identity())) // 第一步分组:按Tracer+日期统计当日Shift数量 .groupBy( (shift, employee, tracer) -> tracer, (shift, employee, tracer) -> shift.getStartDateTime().toLocalDate(), ConstraintCollectors.countLong() ) // 计算当前Tracer当日所需的生产次数(注意转浮点避免整数除法精度丢失) .map((tracer, day, shiftCount) -> { double productionCount = Math.ceil((double) shiftCount / tracer.getBatchesPerProduction()); return new TracerProduction(tracer.getIsotope(), day, (long) productionCount); }) // 第二步分组:按Isotope+日期汇总生产次数 .groupBy( TracerProduction::getIsotope, TracerProduction::getDay, ConstraintCollectors.sumLong(TracerProduction::getProductionCount) ) // 检查是否超出合同上限 .filter((isotope, day, totalProductions) -> totalProductions > isotope.getContract().getMaximumProductionsPerDay()) // 按超出量施加惩罚 .penalizeConfigurableLong("每日生产次数不得超过合同上限", (isotope, day, totalProductions) -> totalProductions - isotope.getContract().getMaximumProductionsPerDay()); } // 辅助类:传递中间计算结果 private static class TracerProduction { private final Isotope isotope; private final LocalDate day; private final long productionCount; public TracerProduction(Isotope isotope, LocalDate day, long productionCount) { this.isotope = isotope; this.day = day; this.productionCount = productionCount; } public Isotope getIsotope() { return isotope; } public LocalDate getDay() { return day; } public long getProductionCount() { return productionCount; } }
3. 自定义可复用ConstraintCollector(可选)
如果需要封装成通用Collector,可基于内置Collector组合实现:
public static ConstraintCollector<Shift, ?, Map<TracerDayKey, Long>> calculateProductionsPerTracerDay() { return ConstraintCollectors.compose( // 第一步:收集Shift对应的Tracer+日期,统计Shift数量 ConstraintCollectors.mapping( shift -> new TracerDayKey(shift.getEmployee().getTracer(), shift.getStartDateTime().toLocalDate()), ConstraintCollectors.countLong() ), // 第二步:转换为生产次数 countMap -> countMap.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> (long) Math.ceil((double) entry.getValue() / entry.getKey().getTracer().getBatchesPerProduction()) )) ); } // 分组键辅助类:必须重写equals和hashCode保证分组正确性 private static class TracerDayKey { private final Tracer tracer; private final LocalDate day; public TracerDayKey(Tracer tracer, LocalDate day) { this.tracer = tracer; this.day = day; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TracerDayKey that = (TracerDayKey) o; return Objects.equals(tracer, that.tracer) && Objects.equals(day, that.day); } @Override public int hashCode() { return Objects.hash(tracer, day); } public Tracer getTracer() { return tracer; } public LocalDate getDay() { return day; } }
关键注意事项
- 整数除法必须转换为浮点类型后计算,否则
Math.ceil会失效(例如5/2整数除法结果为2,转浮点后5.0/2=2.5,向上取整为3); - 自定义分组键必须重写
equals和hashCode,否则分组结果会出现错误; - 优先使用内置Collector组合,比自定义Collector更简洁、可靠;
- 约束流起点尽量选择规划实体(如
Shift),关联关系更清晰。
内容的提问来源于stack exchange,提问作者nogretz
相关产品推荐
相关产品推荐

