People often confuse methods and rules, and new rule users regular ask, "How do I call a rule?" After the last section, you are now feeling like a rule expert and the answer to that is obvious, but let's summarize the differences nonetheless.
public void helloWorld(Person person) { if ( person.getName().equals( "Chuck" ) ) { System.out.println( "Hello Chuck" ); } }
Methods are called directly.
Specific instances are passed.
One call results in a single execution.
rule "Hello World" when Person( name == "Chuck" ) then System.out.println( "Hello Chuck" ); end
Rules execute by matching against any data as long it is inserted into the engine.
Rules can never be called directly.
Specific instances cannot be passed to a rule.
Depending on the matches, a rule may fire once or several times, or not at all.
Earlier the term "cross product" was mentioned, which is the result of a join. Imagine for a moment that the data from the fire alarm example were used in combination with the following rule where there ar no field constraints:
rule when $room : Room() $sprinkler : Sprinkler() then System.out.println( "room:" + $room.getName() + " sprinkler:" + $sprinkler.getRoom().getName() ); end
In SQL terms this would be like doing select * from Room,
Sprinkler
and every row in the Room table would be joined with every
row in the Sprinkler table resulting in the following output:
room:office sprinker:office room:office sprinkler:kitchen room:office sprinkler:livingroom room:office sprinkler:bedroom room:kitchen sprinkler:office room:kitchen sprinkler:kitchen room:kitchen sprinkler:livingroom room:kitchen sprinkler:bedroom room:livingroom sprinkler:office room:livingroom sprinkler:kitchen room:livingroom sprinkler:livingroom room:livingroom sprinkler:bedroom room:bedroom sprinkler:office room:bedroom sprinkler:kitchen room:bedroom sprinkler:livingroom room:bedroom sprinkler:bedroom
These cross products can obviously become huge, and they may very well contain spurious data. The size of cross products is often the source of performance problems for new rule authors. From this it can be seen that it's always desirable to constrain the cross products, which is done with the variable constraint.
rule when $room : Room() $sprinkler : Sprinkler( room == $room ) then System.out.println( "room:" + $room.getName() + " sprinkler:" + $sprinkler.getRoom().getName() ); end
This results in just four rows of data, with the correct Sprinkler
for each Room. In SQL (actually HQL) the corresponding query would be
select * from Room, Sprinkler where Room == Sprinkler.room
.
room:office sprinkler:office room:kitchen sprinkler:kitchen room:livingroom sprinkler:livingroom room:bedroom sprinkler:bedroom
So far the data and the matching process has been simple and small. To mix things up a bit a new example will be explored that handles cashflow calculations over date periods. The state of the engine will be illustratively shown at key stages to help get a better understanding of what is actually going on under the hood. Three classes will be used, as shown below.
public class CashFlow { private Date date; private double amount; private int type; long accountNo; // getter and setter methods here } public class Account { private long accountNo; private double balance; // getter and setter methods here } public AccountPeriod { private Date start; private Date end; // getter and setter methods here }
By now you already know how to create Knowledge Bases and how to
instantiate facts to populate the StatefulKnowledgeSession
, so tables
will be used to show the state of the inserted data, as it makes things
clearer for illustration purposes. The tables below show that a single fact
was inserted for the Account
. Also inserted are a series of debits
and credits as CashFlow
objects for that account, extending over two
quarters.
Two rules can be used to determine the debit and credit for that quarter and update the Account balance. The two rules below constrain the cashflows for an account for a given time period. Notice the "&&" which use short cut syntax to avoid repeating the field name twice.
rule "increase balance for credits" when ap : AccountPeriod() acc : Account( $accountNo : accountNo ) CashFlow( type == CREDIT, accountNo == $accountNo, date >= ap.start && <= ap.end, $amount : amount ) then acc.balance += $amount; end |
rule "decrease balance for debits" when ap : AccountPeriod() acc : Account( $accountNo : accountNo ) CashFlow( type == DEBIT, accountNo == $accountNo, date >= ap.start && <= ap.end, $amount : amount ) then acc.balance -= $amount; end |
If the AccountPeriod
is set to the first quarter we constrain the
rule "increase balance for credits" to fire on two rows of data and
"decrease balance for debits" to act on one row of data.
The two cashflow tables above represent the matched data for the
two rules. The data is matched during the insertion stage and, as you
discovered in the previous chapter, does not fire straight away, but only
after fireAllRules()
is called. Meanwhile, the rule plus its
matched data is
placed on the Agenda and referred to as an Activation. The Agenda is a
table of Activations that are able to fire and have their consequences
executed, as soon as fireAllRules() is called. Activations on the
Agenda are executed in turn. Notice that the order of execution
so far is considered arbitrary.
After all of the above activations are fired, the account has a balance of -25.
If the AccountPeriod
is updated to the second quarter, we have just
a single matched row of data, and thus just a single Activation on the
Agenda.
The firing of that Activation results in a balance of 25.
What if you don't want the order of Activation execution to be arbitrary? When there is one or more Activations on the Agenda they are said to be in conflict, and a conflict resolver strategy is used to determine the order of execution. At the simplest level the default strategy uses salience to determine rule priority. Each rule has a default value of 0, the higher the value the higher the priority. To illustrate this we add a rule to print the account balance, where we want this rule to be executed after all the debits and credits have been applied for all accounts. We achieve this by assigning a negative salience to this rule so that it fires after all rules with the default salience 0.
rule "Print balance for AccountPeriod" salience -50 when ap : AccountPeriod() acc : Account() then System.out.println( acc.accountNo + " : " + acc.balance ); end |
The table below depicts the resulting Agenda. The three debit and credit rules are shown to be in arbitrary order, while the print rule is ranked last, to execute afterwards.
Earlier we showed how rules would equate to SQL, which can often help people with an SQL background to understand rules. The two rules above can be represented with two views and a trigger for each view, as below:
select * from Account acc, Cashflow cf, AccountPeriod ap where acc.accountNo == cf.accountNo and cf.type == CREDIT and cf.date >= ap.start and cf.date <= ap.end |
select * from Account acc, Cashflow cf, AccountPeriod ap where acc.accountNo == cf.accountNo and cf.type == DEBIT and cf.date >= ap.start and cf.date <= ap.end |
trigger : acc.balance += cf.amount |
trigger : acc.balance -= cf.amount |
Drools also features ruleflow-group attributes which allows workflow diagrams to declaratively specify when rules are allowed to fire. The screenshot below is taken from Eclipse using the Drools plugin. It has two ruleflow-group nodes which ensures that the calculation rules are executed before the reporting rules.
The use of the ruleflow-group attribute in a rule is shown below.
rule "increase balance for credits" ruleflow-group "calculation" when ap : AccountPeriod() acc : Account( $accountNo : accountNo ) CashFlow( type == CREDIT, accountNo == $accountNo, date >= ap.start && <= ap.end, $amount : amount ) then acc.balance += $amount; end |
rule "Print balance for AccountPeriod" ruleflow-group "report" when ap : AccountPeriod() acc : Account() then System.out.println( acc.accountNo + " : " + acc.balance ); end |