We've seen how rule conditions are written. So far, all these conditions have filtered data that was inserted in our KieSession
(that is, inserted in the working memory). However, in some situations, we might need to check special conditions on collections that are different from the working memory, such as attributes of some objects, global variables, or subsets of the working memory itself that we can create dynamically. To be able to do so, Drools provides the from
clause, which we can be used to define a specific search space outside of the working memory.
The following rule is a simple example of how we can use the from
clause to look for specific attributes:
rule "For every notebook order apply points coupon" when $o: Order($c: customer, $lines: orderLines) OrderLine($item: item) from $lines Item(name == "notebook") from $item then insert(new Coupon($c, $o, CouponType.POINTS)); end
The previous rule looks for orders in our working memory first. It will store both the order lines collection as a $lines
variable and the customer in a $c
variable (this last variable is stored to use it from the consequence of the rule). After finding an order, it looks in the $lines
variable (which holds all the order lines) and stores the item in another variable. After that, the third line of the rule condition directly searches a single object (the item), and checks whether the item is a notebook.
As you can see, if we only have the Order object in our working memory (and not all of its subcomponents), we can still go deep into its structure in a way that the following conditions are satisfied:
orderLines
attribute in the second part of our rule condition)The from
clause is a very versatile tool. It can be used to get data from multiple sources and not only from attributes. You can invoke methods from global variables that return specific values or lists and use it from the from
clause. You can basically use it with anything that returns a value.
Drools 6.3, and higher, comes with a feature called OOPath expressions, which allows to define the previous rule in a more compact way, as follows:
rule "For every notebook order apply points coupon" when $o: Order($c: customer, /orderLines/item{name == "notebook"} ) then insert(new Coupon($c, $o, CouponType.POINTS)); end
One common use of global variables is to retrieve data that doesn't need to be in the working memory all the time. Thanks to the from
clause, we can directly filter this data from our rules as soon as we execute a global variable method, as follows:
global QueryService qServ rule "get stored coupons for new order" when Order(totalItems > 10, $c:customer) Coupon(customer == $c) from qServ.queryCoupons() then System.out.println("We have a stored coupon"); end
This can be useful to compare the data between the working memory and outside storages, such as databases and web services. We need to be careful when using these external queries by realizing when the engine will invoke the global variable method, though. In the previous example, every time we add a new Order object to the working memory, the first line will be evaluated once again. If the first line fills the required conditions, it will call the second line, invoking the global variable method. If we add 50 orders that fill the first condition, the global variable method will be called 50 times by this rule.
Another thing to be careful about when using from clauses like these is how deep you nest them; if you have to execute five global variable methods one after the other in the same rule and they are all process-intensive, you will have a very slow rule. If you need to do something of this type, it is best to work with caches for your slow methods whenever possible.
Note that if the return value of a global variable method changes, it will not retrigger a rule that invokes it, even if it is the first condition of the rule. This is because global variables are outside the working memory, and therefore, they are not re-evaluated when they change. If you need to re-evaluate things used in the from
clause, the best way to go is to use a subset of the working memory, which you can create using collect or accumulate keywords along with the from
clause. These keywords and their uses are described in the following two sections.
So far, we've seen ways to write conditions in our rules that will get triggered for every single match against the working memory. Let's analyze a simple rule to fully understand this mechanism:
rule "Simple order condition" when $o: Order() then System.out.println("order found: " + $o); end
The previous rule is defined in such a way that if we insert 50 orders in the KieSession, we will trigger this rule 50 times when we fire the rules. This is useful when we want to do something on each element that matches a condition. However, if we want to act against all the elements that match a condition at once, we need an extra syntax to help us. This is where the collect
keyword comes into play.
The "collect" keyword is used to find all the elements that match a specific condition and group them into a collection. Later on, this collection can be assigned to a variable or submitted to further conditions, as follows:
rule "Grouping orders" when $list: List() from collect(Order()) then System.out.println("we've found " +$list.size() + " orders"); end
In the previous case, if we insert 50 orders to KieSession and then fire the rules, the rule will fire only once, producing a list of 50 orders. However, if we fire all the rules without inserting any orders first, this rule would still fire returning an empty list in the $list variable. To avoid so, we should add conditions in the List object by writing it as follows:
$list: List(size > 0) from collect(Order())
The collect
keyword can be combined with the from
clause to create new collections from different sources. It is very useful whenever we can find objects through the rule conditions and add them directly to a collection.
In the previous section, we saw that the collect
keyword allows us to gather information directly from a source into a collection. This is a very useful component when we already have the elements as we need them to be somewhere accessible in the working memory. However, sometimes we need to apply transformations to the data that matches a condition. For these cases, the accumulate
keyword is used to transform every match of a condition to a specific type of data.
You can think of the accumulate
keyword as a collect
on steroids. You not only get to group every item that matches a condition, but also extrapolate the data from these elements in a specific programmatic way. Some common examples where the accumulate keyword is used are counting elements that match a specific condition, get average values of an attribute of certain type of objects, and to find the average, minimum, or maximum values of a specific attribute in a condition.
Perhaps an example could help clarify the use of accumulate. In the previous rules, we've used a method of the Order object, called getTotalItems
, to return the count of items that we were purchasing in this order. So far, this has been the only way we would have to get this information from the Order object. However, using accumulate, we can obtain this information while filtering specific items using the full power of rules. Let's see this in an example:
rule "10+ items with sale price over 20 get discount" when$o: Order($lines: orderLines) Number(intValue >= 10) from accumulate(OrderLine(item.salePrice > 20.00, $q: quantity) from $lines,init(int count = 0;),action(count += $q),reverse(count -= $q),result(count)) then $o.increaseDiscount(0.05); end
We've got a lot to explain from the previous rule. It's main objective is to get the total number of items in the order that follows a specific condition: having a sale price value higher than 20. The getTotalItems method would be able to give us a count of all the items, however, it wouldn't be able to filter through them. The accumulate here, however, would allow us to apply a condition to the elements and apply a predicate to transform the elements that match the accumulate condition in the information that we need. Let's analyze the parts of the accumulate to fully understand its structure, as follows:
$lines
variable), checks whether the sale price is over 20 and stores the quantity attribute value in a $q variable to be used later. Notice how you have a second from
clause in the accumulate as you can nest them when needed.The following four parts of the accumulate (init, action, reverse, and result) contain information about what to do whenever we find an object that matches the condition:
action
code blocks have been called accordingly, the result
code block will return the specific data that we collected, which will dictate the type that we'll write on the right-hand side of the from accumulate
expression. In our example, we return the count value (an integer value), which is matched against the first part of the accumulate function: Number(intValue > 10)
.Although this is the full syntax for using accumulate
, it doesn't necessarily have to be this complex every single time. Drools provides a set of predefined accumulate functions that you can directly use in your rules. Here's the previous rule that is rewritten to show how to use one of these built-in functions:
rule "10+ items with sale price over 20 get discount" when $o: Order($lines: orderLines) Number(intValue >= 10) from accumulate(OrderLine(item.salePrice > 20.00, $q: quantity) from $lines,sum($q)) then $o.increaseDiscount(0.05); end
The already provided accumulate functions are as follows:
Using a predefined function is preferred as it makes the rule more readable and is less prone to errors. You can even use many of them at once, one after the other, separated by a comma, as shown in the following example:
rule "multi-function accumulate example" when accumulate(Order($total: total),$maximum: max($total),$minimum: min($total),$average: avg($total)) then //... end
The previous rule stores the maximum, minimum, and average of the totals of all the orders in the working memory. As you can see, the from
clause is not mandatory for the accumulate
keyword. It is even discouraged with multifunction accumulates as it might return completely different types of objects (such as Lists
and Numbers
).
If you want to do an accumulate in a way that is not provided by these functions, Drools provides an API to create our own accumulate functions, similar to that used to create our own custom operators. We will see more about the API in the next chapter.
The previous sections have been about how to extract data, however, we've been very straightforward with the order of our conditions. Always every condition has been separated by a comma and the execution of the rule was basically each condition followed by an implicit AND. It is time to see how to apply more complex boolean operations to our rules.
We've seen how to use boolean operations in a single object, checking whether its internal attributes matched the specific combinations of conditions. We'll see now how to do the same with different objects and how it would get translated to the rule language.
Whenever we've written a condition so far, it's been translated to a search for any object (or group of objects) that matches the condition in the working memory. If we precede the condition of an object (or group) with the not
keyword, we can check whether no incidence of the condition is found in the working memory and trigger a rule for these cases. Let's see the following example of this keyword:
rule "warn about empty working memory" when not(Order() or Item()) then System.out.println("we don't have elements"); end
The previous rule uses the not keyword to make sure that at the moment of firing the rules, we have at least an Order or Item object. If we don't, it will print a warning.
Notice how we use the OR keyword to share a condition in not
. This is because the NOT keyword will contain a boolean expression. In this case, since we want to search for both Orders or Items to exist in the working memory, we can group them together in a single expression inside NOT. Another way of writing this rule would be as follows:
rule "warn about empty working memory" when not(Order()) not(Item()) then System.out.println("we don't have elements"); end
It would act in the same way.
In a similar fashion to the not
keyword, we have a few other keywords that we can use to check the existence of elements in the working memory. The exists
keyword is used for the purpose of checking whether any object is present in the working memory that fulfills the condition in it.
Let's take a look at the following two rules, one with exists and one without it:
rule "with exists" when exists(Order()) then System.out.prinltn( "We have orders"); end
|
rule "without exists" when $o: Order() then System.out.println($o); end
|
The main difference between them is how many times these rules will fire for one or many Order objects. For the case on the right-hand side, if you have multiple orders, the rule will fire once for every order. On the left-hand side, whether you have one element, five, or five million Orders, you will fire the rule once.
The other difference that you can see in the declaration is that the rule on the right-hand side has a variable declaration. This can be done only in the case of the right-hand side components as it will execute for every Order in the working memory and you can have a reference to each one of them. In the case of the left-hand side component, it is only checking for a boolean expression (whether or not the Order objects exist in the working memory), so it is not storing any reference we could use in the consequence of the rule.
A similar pattern is used in the forall
keyword. When using forall
, we check two conditions against the working memory. Any object that matches the first condition must match the second condition to make forall
true.
Let's see the following example of forall
and exists working together:
rule "make sure all orders have at least one line" when exists(Order()) forall(Order()Order(orderLines.size() > 0) ) then System.out.println("all orders have lines"); end
In the previous rule, you can see the first condition uses exists to check whether there's any Order object in the working memory. If there is one order or a thousand, this will evaluate once.
The second part of the rule is using two conditions. For every item that fills the first condition (being an Order object), it will also need to fill the second one (having at least one order line). This means that every order in the working memory must have at least one order line for this rule to fire.
There's a reason for using both exists
and forall
in the same rule, beyond just providing a short example of both. The forall
structure checks whether the collection of objects that fills both conditions are the same. This means if nothing fills the first and second conditions of the forall
, it will evaluate to true. To avoid this, before every forall
, we usually use exists
with the first condition that we will use in the forall
.
You can find examples of running rules about these advanced conditional elements in ConditionalElementsTest
of the code base.
3.144.109.34