Working memory breakdown: the from clause

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:

  • It is easy to read and break down into multiple conditions
  • It can take into account the situations where we might have collections of objects to dig into (such as the orderLines attribute in the second part of our rule condition)
  • It can make a rule easier to read

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.

Tip

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.

Collect from objects

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.

Accumulate keyword

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:

  • $o: Order($lines: orderLines): This part is simple. We're checking every Order object that we have in our working memory and storing the order lines attribute in a variable for later use.
  • Number(intValue >= 10): Our rule is trying to check whether we have at least, 10 items with price higher than 20. This first part of the accumulate line is checking the return value from the accumulate. It can be anything that you want, but for this first example, it is going to be a number. If that number is 10 or more, we consider the return value of the accumulate to be fulfilling our rule. This means that the accumulate should count all the items that match the specific condition.
  • Condition inside accumulate: In the accumulate, we have a first part that matches a specific condition. In this case, it goes all over the OrderLine objects of the order (which were stored in the $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:

  • init: This is done at the beginning of the accumulate. It initializes the variables to be used every time we find a match in the accumulate condition. In our case, we're initializing a count variable.
  • action: Whenever we find an element in the working memory that matches the accumulate condition, the rule engine will trigger the action section of code in the accumulate. In our example, we're increasing the count of the elements by the value of the $q variable, which holds the quantity of the OrderLine object.
  • reverse: This is an optional code block. As every object inserted, modified, or deleted from the working memory might trigger the condition (and activate the action code block), it can also make an element that had already matched the accumulate condition to stop doing so. In order to increase the performance, the reverse code block can be provided to avoid having to recalculate all the accumulate again if it is too processor-intensive. In our example, whenever OrderLine stops matching the condition, the reverse code will just decrease the count variable by the amount of the $q variable that is previously stored.
  • result: This code block holds the return variable name or formula for the finished accumulate section. Once every object that matches a condition has been processed and all the 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:

  • count: This keeps a count of a variable that matches a condition
  • sum: This sums up a variable, as shown in the previous example
  • avg: This gets the average value of a variable for all the matches in the accumulate
  • min: From all the variable values obtained in the accumulate condition, this returns the minimal one
  • max: From all the variable values obtained in the accumulate condition, this returns the maximum one
  • collectList: This stores all the values of a specified variable in the condition in an ordered list and returns them at the end of the accumulate
  • collectSet: This stores all the values of a specified variable in the condition in a unique elements' set and returns them at the end of the accumulate

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.

Advanced conditional elements

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.

NOT keyword

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.

EXISTS and FORALL keywords

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.109.34