DroolsではDRLという記法を用いて、アプリケーション内で実行するルールを記述できます。
DRL記法のなかには、ルールエンジンに投入されたデータを効率的に扱うために様々な構文がサポートされています。
本記事ではaccumulate, groupbyという二つの構文について、サンプルコードを交えて紹介していきます。
はじめに
Droolsではルールの定義に、以下のようなDRL記法を使用します。
when節にはルールの条件を、then節にはルールの条件にマッチした場合に実行するアクションを記載します。
rule "Child"
when
$person : Person( age < 20 )
$drink : Drink()
then
$drink.setName( "Orange Juice" );
$drink.setCharge( 100 );
end
この例は、「年齢が20未満のPersonオブジェクトと、Drinkオブジェクトがルールに渡された」ときに、「JavaのSetterメソッドでデータの更新を行う」というルールです。
単純なデータ更新ではこのような記述で実現可能ですが、実際の業務ではもっと複雑なデータ処理をするケースもあると思います。
DRLでは、複数のデータの集約・集計が行えるように、accumlate, groupbyという構文がサポートされています。
accumulate
ルールエンジンに渡された複数のデータを集約して、一括で処理を行うための構文です。
以下のような形式で記述します。
accumulate( SOURCE_PATTERN; FUNCTIONS [;CONSTRAINTS] )
SOURCE_PATTERN
: accumulate処理の対象とするデータを設定します
FUNCTIONS
: SOURCE_PATTERNのデータに対して実施する処理を設定します(average
(平均値の計算), min
(最小値の計算), max
(最大値の計算)、等が利用可能です)
CONSTRAINTS
: FUNCTIONSの結果を使ったルールの条件を記述できます(任意項目)
groupby
groupbyは、Drools version 8.41.0.FINALから新たに追加された構文です。
ルールエンジンに渡されたデータをなんらかの属性でグループ分けし、各グループごとにaccumulate処理を実行することができます。
以下の形式で記述します。
groupby( SOURCE_PATTERN; KEY_FUNCTION ; FUNCTIONS [;CONSTRAINTS] )
SOURCE_PATTERN
: groupby構文で取り扱いたいデータを設定します
KEY_FUNCTION
: SOURCE_PATTERNのデータをグループ分けするために、キーとして利用したいデータを設定します
FUNCTIONS
: グループ分けしたSOURCE_PATTERNのデータに対して、実施する処理を設定します(accumulateのFUNCTIONSで使用できる関数が利用可能です)
CONSTRAINTS
: FUNCTIONSの結果を使ったルールの条件を記述できます(任意項目)
サンプルコード
ここからは、accumulate, groupbyを使用したサンプルコードを紹介していきます。
学生の試験結果の平均点を集計する題材にしています。
コード全量はGithubにあげています。
まず、学生の試験結果を保持するJavaクラスを作成します。
学生の名前・学級・試験結果の点数をフィールドとして定義しています。
package org.example.drlgroupby;
public class StudentTestScore {
private String studentName;
private String studentClass;
private int testScore;
...
accumulateでテストの平均点を集計する
渡された試験結果から平均点を計算するルールを記述します。
import org.example.drlgroupby.StudentTestScore
rule "Calculate overall average"
when
accumulate( StudentTestScore( $score : testScore );
$avg : average( $score ) )
then
System.out.println("Overall Average: " + $avg);
end
accumulate構文では、取り扱うデータとしてStudentTestScore
を指定しており、average()関数を使って平均点を計算するようにしています。
then節で計算した平均点を標準出力します。
テストコードでルールを実行してみましょう。
...
@Test
public void test_学年全体の平均点を計算する() {
// set up
var taro = new StudentTestScore("Taro", "A", 100);
var jiro = new StudentTestScore("Jiro", "A", 75);
var saburo = new StudentTestScore("Saburo", "B", 70);
kieSession.insert(taro);
kieSession.insert(jiro);
kieSession.insert(saburo);
// execute
int count = kieSession.fireAllRules();
// assert
assertEquals(count, 2);
}
...
ここでは3人の学生の試験結果をKieSessionに投入しています。
テストを実行すると、以下の文言が標準出力されます。
Overall Average: 81.66666666666667
3人分の試験結果の平均点が算出されていることがわかります。
groupbyで平均点が80点以上の学級を特定する
続いて、groupbyを用いて学級ごとに平均点を計算し、計算結果が80点を超えている学級を特定してみましょう。
ルールは以下のように定義します。
import org.example.drlgroupby.StudentTestScore
rule "Filter classes whose average is greater than 80"
when
groupby(
$s : StudentTestScore( $score : testScore );
$group : $s.getStudentClass();
$ave : average($score);
$ave > 80)
then
System.out.println("Class " + $group + " average is greater than 80.");
end
groupbyの対象データとしてStudentTestScoreを指定しているのはaccumulateと同様です。
KEY_FUNCTION
として、$group : $s.getStudentClass();
と記述することで、StudentTestScoreクラスのstudentClass(学級)
フィールドでデータのグルーピングができるようにしています。
さらにFUNCTIONS
としてaverage関数を記述しているため、各グループごとに平均値が計算されます。
最後に、CONSTRAINTS
として$ave > 80
を設定しています。
このようにすることで、前段で計算した平均値が80を超えている学級に対してのみ、then節のアクションを実行できます。
では、テストコードでルールを実行します。
ここでは学級Aの生徒二人、学級Bの生徒二人の試験結果をKieSessionに投入しています。
学級Aの平均点が80点超え、学級Bの平均点は80点未満となるようなデータとしています。
...
@Test
public void test_平均点が80点超のクラスを抽出する() {
// set up
var taro = new StudentTestScore("Taro", "A", 100);
var jiro = new StudentTestScore("Jiro", "A", 75);
var saburo = new StudentTestScore("Saburo", "B", 89);
var shiro = new StudentTestScore("shiro", "B", 60);
kieSession.insert(taro);
kieSession.insert(jiro);
kieSession.insert(saburo);
kieSession.insert(shiro);
// execute
int count = kieSession.fireAllRules();
// assert
assertEquals(count, 2);
}
...
テストを実行すると、以下の文言がコンソール出力されます。
Class A average is greater than 80.
想定どおり、学級Aの平均が80点超である旨のメッセージが表示されていますね。