【Drools】DRLのaccumulate・groupby構文で複数データを扱う

DroolsではDRLという記法を用いて、アプリケーション内で実行するルールを記述できます。
DRL記法のなかには、ルールエンジンに投入されたデータを効率的に扱うために様々な構文がサポートされています。
本記事ではaccumulate, groupbyという二つの構文について、サンプルコードを交えて紹介していきます。

- [はじめに](#はじめに)
- [accumulate](#accumulate)
- [groupby](#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](https://github.com/Hami1ton/study_brms/tree/main/drools/drl_groupby)にあげています。

まず、学生の試験結果を保持するJavaクラスを作成します。
学生の名前・学級・試験結果の点数をフィールドとして定義しています。
```StudentTestScore.java
package org.example.drlgroupby;

public class StudentTestScore {

    private String studentName;

    private String studentClass;

    private int testScore;
...
```

## accumulateでテストの平均点を集計する

渡された試験結果から平均点を計算するルールを記述します。
```AccumulateSample.drl
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節で計算した平均点を標準出力します。

テストコードでルールを実行してみましょう。
```AccumulateSampleTest.java
...
    @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点を超えている学級を特定してみましょう。
ルールは以下のように定義します。
```GroupbySample.drl
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点未満となるようなデータとしています。
```GroupbySampleTest.java
...
    @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点超である旨のメッセージが表示されていますね。


# 参考

- [Drools Rule Language (DRL)](https://docs.drools.org/8.41.0.Final/drools-docs/drools/language-reference/index.html)
- [groupby(kie-blog)](https://blog.kie.org/2023/07/groupby-a-new-way-to-accumulate-facts-in-drl.html)
- [本記事で紹介したサンプルコード](https://github.com/Hami1ton/study_brms/tree/main/drools/drl_groupby)