本文将介绍一下Drools7中accumulate的用法及其在遍历List时的一些问题,同时也会分享几个使用场景。
虽然 accumulate 虽然只是Drools中的一个关键字,但是它的功能很强大,在实际开发中我用于统计、多个List集合交叉校验、数据预处理、甚至是运行调试。但是个人感觉有些细节很难把握到,例如:循环;所以这篇文章写得比较啰嗦,既然要搞清楚它,那就请按捺住浮躁的情绪。
一、语法
语法结构:
accumulate( <source pattern>; <functions> [;<constraints>] )
accumulate 可以使用内置函数,也可以用自定义函数。
1.内置函数的使用,求和函数sum的使用例子如下:
entity
public class Order {
private Integer id;
private Integer customerId;
private Double total;
private Double discount;
private Double pay;
private Date createDate;
private List<Item> items;
}
rule
//所有订单的金额低于800时,触发规则
rule "order_0"
when
accumulate(
Order(total>0.00, $total:total);
$sum:sum($total);
$sum <800.00
)
then
System.out.println("销售业绩太差了");
end
//所有订单的金额低于800时,触发规则,并返回合计金额
rule "order_0"
enabled false//rule可触发状态,false为不可触发但是会匹配,true为可触发,默认为true,
when
//doubleValue > 0.00 表示返回值要大于0.00
$n:Number(doubleValue > 0.00) from accumulate(
Order(total>0.00, $total:total),//注意这里是逗号,不是分号
sum($total)//当作为返回值时,不能对函数绑定引用;
)
then
System.out.println($n);
end
内置函数还有以下几个,就不一一举例了:
average 求平均数
min 最小值
max 最大值
count 计数器
variance 方差,这里用的是增量方差算法
standardDeviation 标准差,继承了variance函数方法 ,直接对variance函数 结果开平方
collectList 数据列表收集,这个翻译有点晦涩,其实就是把符合条件的source归集起来,可接受重复数据
collectSet 数据列表收集。
2.自定义accumulate函数
有两种自定义accumulate函数的方法。
第一种:只需要创建一个java类并实现org.kie.api.runtime.rule.AccumulateFunction接口,然后在.drl文件中impor就可以了。
对import的语法结构做一下简要说明 import accumulate <class_name> <function_name> ,<class_name>必须是自定义函数类的完整包名+类名,<function_name>是给自定的函数设置个名字,然后函数就用这个<function_name>调用自定义函数,语法与调用内置函数一样。
这里由于篇幅和主题问题就不再细说了,这种方式的优点是代码可以重复利用,便于维护使用该函数的规则,但是我个人在实际开发当中业务逻辑不尽相同,很难做到重复利用,硬套这种方式不是不可以,但是工作量还是挺大的,自定义实现AccumulateFunction接口的细节可以点击这里查看,因为篇幅原因这里就不再写了,后便将会再介绍另一种语法结构,它会更适合我的实际开发的情况。
第二种:这里其实不是自定义accumulate函数了,而是另一种语法结构,之所以放在这里是为了让知识点更紧凑。
语法结构如下:
<result pattern> from accumulate( <source pattern>,
init( <init code> ),
action( <action code> ),
reverse( <reverse code> ),
result( <result expression> ) )
<source pattern>:数据源,从工作内存或绑定变量中获取目标资源,如:Person(age >=18)将会获取工作内存中所有 age>=18的Person实例,也就是说如果工作内存中存在多个Person实例时会多次匹配评估所有实例,但会生成 一个规则实例(一条(组)数据或一个(组)实例与规则匹配叫做一个规则匹配或规则实例,规则实例 会各自 独立的放在agenda中,由agenda分别执行),然后触发一次规则(个人理解的触发规则指的是每次由agenda 执行一次就是触发一次规则。也可以使用collect先将Person收集到一个List集合中,然后处理List集合,这样也 只触发一次规则 如:$li:List() from collect (Person(age >=18)),然后只在<action code>中处理 $li 即可。
<init code>:可以理解成初始化代码块吧,对每组<source pattern>执行一次,且在遍历<source pattern>前执行;可以在这里定 义当前<source pattern>的全局共享变量,变量的生命周期直到<source pattern>遍历结束为止。
<action code>:自定义的核心逻辑都会在这里实现,它会对<source pattern>中每个元素的执行一次;例如:设置Order是否为 高额订单。在这里也可以实现统计、代码调试、数据预处理等功能。
<reverse code>:可选,可用于action的反向计算,数据发生变更,只会对accumulate处理过的数据且变更字段必须出现在条件或出现过引用时才会生效。如果没有设置reverse的话,当accumulate处理过的数据发生变更时,action会重新计算。又是一段很晦涩的描述,下文会举例说明。
<result expression>:返回结果值,这里可以是确切的结果值,也可以是表达式。
这里给出一个例子,用于说明这种结构的用法,以下所有案例都用的statefullKiesession:
entity:
public class Item {
private Integer id;
private Integer orderId;//订单编码
private String goodId;//商品编码
private String name;//商品名称
private Double price;//单价
private Double count;//数量
private Order order;
//getter、setter略
}
案例一:
rule_1:
//遍历orderId == 1的Item并累加其price并输出结果
rule "order_4"
salience 10
when
$n:Number() from accumulate(
$item1:Item( orderId == 1,$value:price),
init(Integer i =0;Integer oI = 0;List inlist = new ArrayList();double total = 0;),
action(
System.out.println("action println before:"+total);
total+=$value;
System.out.println("action println after:"+total);
),
result(total)
)
then
System.out.println("then print:"+$n);
end
插入数据源_1:
Item item = new Item(1,1,2.0);
Item item2 = new Item(2,1,3.0);
kieSession.insert(item);
kieSession.insert(item2);
kieSession.fireAllRules();
执行结果_1:
action println before:0.0
action println after:3.0
action println before:3.0
action println after:5.0
then print:5.0
工作内存中两条orderId = 1依次被action处理,累加了price并打印了最终值。这个很简单没什么可说的,接下来我们会修改工作内存中的数据,看accumulate又会有什么样的行为。
案例二
rule_2:
rule "order_4"
salience 10
when
$n:Number() from accumulate(
$item1:Item( orderId == 1,$value:price),
init(Integer i =0;Integer oI = 0;List inlist = new ArrayList();double total = 0;),
action(
System.out.println("action println before:"+total);
total+=$value;
System.out.println("action println after:"+total);
),
result(total)
)
then
System.out.println("then print:"+$n);
end
//修改id=1的Item的price属性为10.00,将会造成order_4 规则的重新评估和触发
rule "update"
salience 1
when
$i:Item(id ==1)
then
modify($i){setPrice(10.00)};
end
插入的数据源不变,执行结果如下:
action println before:0.0
action println after:3.0
action println before:3.0
action println after:5.0
then print:5.0
action println before:0.0
action println after:3.0
action println before:3.0
action println after:13.0
then print:13.0
以上结果显示accumlate对工作内存中的两条Item的price实现了累加,后因为update规则修改并更新了fact,导致accumulate再次触发规则,重新计算了结果。
需要注意的是:1、如果update规则modify ($i){setPrice ( 2.00)};即值没变的时候,同样会有上述的执行规程,但是modify ($i){};时却不执行重新评估,update($i);又会导致死循环。(题外话:modify和update虽然都是用于修改工作内存)
2、当modify ($i){setId ( 10)};是不会触发规则重新触发的,只有更新的属性在accumulate函数中作为条件(例如:orderId == 1)或有引用(例如:$value:price)时才会触发规则额的重新触发。为了缩短篇幅,例子的话这里就不写了。
案例三:
试想下rule_2的执行过程,如果工作内存中的Item数量很多的话,只更新其中一个Item就要重新遍历所有Item重新累加price,那性能可想而知。这时候就需要用<reverse code>来优化accumulate函数了,同样给出一个例子:
rule_3:
rule "order_4"
salience 10
when
$n:Number() from accumulate(
$item1:Item( orderId == 1,$value:price),
init(Integer i =0;Integer oI = 0;List inlist = new ArrayList();double total = 0;),
action(
System.out.println("action println before:"+total);
total+=$value;
System.out.println("action println after:"+total);
),
reverse(
//这里做了action的反向操作
System.out.println("reverse println before:"+total);
total-=$value;
System.out.println("reverse println after:"+total);
),
result(total)
)
then
System.out.println("then print:"+$n);
end
rule "update"
salience 1
when
$i:Item(id ==1)
then
modify($i){setPrice(10.00)};
System.out.println("update price");
end
数据源依然不变,执行结果如下:
action println before:0.0
action println after:3.0
action println before:3.0
action println after:5.0
then print:5.0
update price
reverse println before:5.0
reverse println after:3.0
action println before:3.0
action println after:13.0
then print:13.0
从以上执行结果可以看出,accumulate函数先遍历累加了Item的price并输出了结果;然后执行“update”规则后,accumulate函数中的reverse只对发生变更的Item做了反向累加操作,然后再依据反向累加后的结果累加了更新后的Item,这样性能是不是提高了很多。需要注意的是reverse只会对accumulate函数处理过的数据起作用。
对于未经过accumulate函数处理过的数据发生变更或者有insert新数据到工作内存的时候,action并不会重新遍历所有Item,只是再原有的累加结果上再累加这些数据而已,同样给出例子证明:
案例四:
rule_4
rule "order_4"
salience 10
when
$n:Number() from accumulate(
$item1:Item( orderId == 1,$value:price),
init(Integer i =0;Integer oI = 0;List inlist = new ArrayList();double total = 0;),
action(
System.out.println("action println before:"+total);
total+=$value;
System.out.println("action println after:"+total);
),
reverse(
System.out.println("reverse println before:"+total);
total-=$value;
System.out.println("reverse println after:"+total);
),
result(total)
)
then
System.out.println("then print:"+$n);
end
rule "update"
salience 1
when
$i:Item(id ==3)
then
modify($i){setOrderId(1)};
System.out.println("update OrderId");
end
变更数据源中已有数据,插入数据源
Item item = new Item(1,1,2.0);
Item item2 = new Item(2,1,3.0);
Item item3 = new Item(3,2,4.0);//注意这里有一个orderId= 2的Item
kieSession.insert(item);
kieSession.insert(item2);
kieSession.insert(item3);
kieSession.fireAllRules();
执行结果:
action println before:0.0
action println after:3.0
action println before:3.0
action println after:5.0
then print:5.0
update OrderId
action println before:5.0
action println after:9.0
then print:9.0
从以上结果可以看出,变更未经accumulate处理过的Item并不会触发reverse,action也只是在已有的结果的基础上累加了新数据,所以这里并不用担心性能问题。
案例五:
rule_5
rule "order_4"
salience 10
when
$n:Number() from accumulate(
$item1:Item( orderId == 1,$value:price),
init(Integer i =0;Integer oI = 0;List inlist = new ArrayList();double total = 0;),
action(
System.out.println("action println before:"+total);
total+=$value;
System.out.println("action println after:"+total);
),
reverse(
System.out.println("reverse println before:"+total);
total-=$value;
System.out.println("reverse println after:"+total);
),
result(total)
)
then
System.out.println("then print:"+$n);
end
rule "update"
salience 1
when
eval(true)
then
insert(new Item(5,1,20.00));
System.out.println("insert Item");
end
insert新数据到工作内存,数据源不变,执行结果如下:
action println before:0.0
action println after:3.0
action println before:3.0
action println after:5.0
then print:5.0
insert Item
action println before:5.0
action println after:25.0
then print:25.0
从上述执行结果可以看出insert的新数据也只是累加操作而已。
补充内容:
这里在分享一个用accumulate函数做测试的案例,证明“enabled false” 是在when执行之后。
entity和数据源跟之前的案例一样,rule修改如下:
rule "order_4"
enabled false//禁止触发rule
salience 10
when
$n:Number() from accumulate(
$item1:Item( orderId == 1,$value:price),
init(Integer i =0;Integer oI = 0;List inlist = new ArrayList();double total = 0;),
action(
System.out.println("action println before:"+total);
total+=$value;
System.out.println("action println after:"+total);
),
reverse(
System.out.println("reverse println before:"+total);
total-=$value;
System.out.println("reverse println after:"+total);
),
result(total)
)
then
System.out.println("then print:"+$n);
end
执行结果如下:
action println before:0.0
action println after:3.0
action println before:3.0
action println after:5.0
从执行结果可以看到,when中的accumulate函数执行了action中的打印,但是没有执行then里的打印,说明enabled false属性是在when执行之后才执行的。事实上所有的attribute都是在when之后执行,官网介绍的attributes的值可以适用when中的绑定变量也能证实这一点。
其实accumylate就像是嵌入在when中的java代码,就想jsp中嵌入java代码一样。这里我认为只要accumulate函数的返回结果是个恒为true的boolean值,就可以做任何事情,测试也只是其中的一个想法而已。
尽管drools 的基本思想是把复杂的业务拆分为更小的规则单元再用冲突策略和优秀的算法,化整为零简化规则管理和控制,但是 accumulate函数对于规则的管理和性能提升的作用还是值得研究一下的。如果各位同学有什么好的想法,欢迎指点。
文章写的有点乱而且各知识点切换的很僵硬,对于写博文实在没有经验,欢迎懂得的同学指教我一下。先谢谢了!