Groovy で JBoss Drools を使う - ルールエンジン機能

JBoss Drools はルールエンジン(Drools Expert)の機能と CEP(Drools Fusion)*1 の機能を兼ね備えており、なかなか興味深いツールです。(さらに BPMN2 等も処理できる)

というわけで今回は Drools のルールエンジン機能を Groovy で簡単に試してみました。(CEP 機能は次回 id:fits:20120105)

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20120104/

Drools を使うための @Grab 定義

Groovy で Drools を使用するには最低限以下の 3つのモジュールを @Grab で定義する必要がありました。

ちなみに drools-compiler と jaxb-xjc を @Grab で定義していないと、ClassNotFoundException が発生して KnowledgeBuilderFactory.newKnowledgeBuilder() の実行に失敗するのでご注意ください。

Drools を使うための @Grab 定義例
@Grab("org.drools:drools-core:5.4.0.Beta1")
@Grab("org.drools:drools-compiler:5.4.0.Beta1")
@Grab("com.sun.xml.bind:jaxb-xjc:2.2.5-b09")

ルール定義(.drl ファイル)

それではルール定義を行います。
今回はオーソドックスな .drl ファイルを使って以下のようなルールを定義してみました。

  • 5千円以上(1万円未満)で5百円引
  • 1万円以上(1万5千円未満)で千円引
  • 1万5千円以上で2千円引

.drl ファイルでのルール定義は以下のように実装します。

  • rule ブロックにルールを定義
  • when ブロックに条件を定義
  • then ブロックに条件を満たした場合の処理を定義
  • no-loop で同じルールの再適用を防止
  • salience で優先度を指定
  • update で更新後のデータを使ってルールを再適用

ここで以下の点に注意が必要です。

  • 条件に合致した全てのルールが優先度順に適用される
  • 後ろに記載しているルールの方が優先される
  • update を実行すると no-loop が無ければ同じルールも再適用される

後ろのルールが優先されるため、実のところ下記サンプルでの salience は不要です。(no-loop も実は不要)
また、discountPrice == 0 の条件が無いと無限ループになる点に注意が必要です。(subTotalPrice が 10000 の場合に 5百円引 と 千円引 のルールを交互に適用し続ける事になる)

さらに update の実行が無いと、discountPrice を更新する前のデータ(discountPrice = 0)を使って条件に合致する全ルールを適用する事になるので、1万円以上でも最終的に "5千円以上で5百円引" が適用されて 5百円引 の結果になってしまう点にご注意ください。

order_discount.drl
package fits.sample

import java.math.BigDecimal

dialect "mvel"

rule "5千円以上で5百円引"
    no-loop
    when
        $order : Order(subTotalPrice >= 5000, discountPrice == 0)
    then
        System.out.printf("5百円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(500)
        update($order)
end

rule "1万円以上で千円引"
    no-loop
    salience 10

    when
        $order : Order(subTotalPrice >= 10000, discountPrice == 0)
    then
        System.out.printf("1千円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(1000)
        update($order)
end

rule "1万5千円以上で2千円引"
    no-loop
    salience 20

    when
        $order : Order(subTotalPrice >= 15000, discountPrice == 0)
    then
        System.out.printf("2千円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(2000)
        update($order)
end

実のところ、単一ルールのみ適用されるようにすれば余計な処理が不要となり、上記と同等の処理が以下のように簡単になります。

order_discount2.drl(シンプル版)
・・・
rule "5千円以上で5百円引"
    when
        $order : Order(subTotalPrice >= 5000 && subTotalPrice < 10000)
    then
        System.out.printf("5百円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(500)
end

rule "1万円以上で千円引"
    when
        $order : Order(subTotalPrice >= 10000 && subTotalPrice < 15000)
    then
        System.out.printf("1千円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(1000)
end

rule "1万5千円以上で2千円引"
    when
        $order : Order(subTotalPrice >= 15000)
    then
        System.out.printf("2千円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(2000)
end


なお、Beta 版を使ったからなのか、ルールエンジン機能用の .drl ファイルでは then ブロック内の処理でセミコロン(;)は特に必要ありませんでした。(ただし、CEP機能用の .drl ファイルでは必須でした)

StatelessKnowledgeSession を使ったルールエンジン処理

Drools の基本的な処理は以下のような手順で実装します。

  1. KnowledgeBuilderFactory から KnowledgeBuilder 生成
  2. KnowledgeBuilder にリソース(ルール定義)追加
  3. KnowledgeBaseFactory から KnowledgeBase 生成
  4. KnowledgeBase に KnowledgeBuilder から取得した KnowledgePackages を設定
  5. KnowledgeBase から StatefulKnowledgeSession や StatelessKnowledgeSession を生成
  6. StatefulKnowledgeSession や StatelessKnowledgeSession を使って処理を実施

まずは、状態を保持しない StatelessKnowledgeSession を使ったサンプルスクリプトです。

StatelessKnowledgeSession では execute に処理対象のデータを渡す事でルールエンジン処理を実施します。(execute は何回でも呼び出せます)

order_discount.groovy
package fits.sample

@Grab("org.drools:drools-core:5.4.0.Beta1")
@Grab("org.drools:drools-compiler:5.4.0.Beta1")
@Grab("com.sun.xml.bind:jaxb-xjc:2.2.5-b09")
import org.drools.KnowledgeBaseFactory
import org.drools.builder.KnowledgeBuilderFactory
import org.drools.builder.ResourceType
import org.drools.io.ResourceFactory

class Order {
    String name
    BigDecimal subTotalPrice = BigDecimal.ZERO
    BigDecimal discountPrice = BigDecimal.ZERO

    BigDecimal totalPrice() {
        subTotalPrice.subtract(discountPrice)
    }
}

def builder = KnowledgeBuilderFactory.newKnowledgeBuilder()
//ルール定義の追加
builder.add(ResourceFactory.newClassPathResource("order_discount.drl", getClass()), ResourceType.DRL)

if (builder.hasErrors()) {
    //エラー発生時の処理
    println builder.errors
    return
}

def base = KnowledgeBaseFactory.newKnowledgeBase()
base.addKnowledgePackages(builder.getKnowledgePackages())

//状態を保持しない StatelessKnowledgeSession 生成
def session = base.newStatelessKnowledgeSession()

def orders = [
    new Order(name: "order1", subTotalPrice: new BigDecimal(2000)),
    new Order(name: "order2", subTotalPrice: new BigDecimal(5000)),
    new Order(name: "order3", subTotalPrice: new BigDecimal(10000)),
    new Order(name: "order4", subTotalPrice: new BigDecimal(13000)),
    new Order(name: "order5", subTotalPrice: new BigDecimal(18000))
]

//ルールエンジン処理の実行
session.execute(orders)

println "--------------------"

orders.each {
    println "${it.name} : ${it.totalPrice()}"
}
実行例
> groovy order_discount.groovy
2千円引 : order5
1千円引 : order4
1千円引 : order3
5百円引 : order2
--------------------
order1 : 2000
order2 : 4500
order3 : 9000
order4 : 12000
order5 : 16000

StatefulKnowledgeSession を使ったルールエンジン処理

次に状態を保持する StatefulKnowledgeSession を使ったサンプルスクリプトです。(StatefulKnowledgeSession を使った処理の部分だけが異なる)

execute を呼び出すだけの StatelessKnowledgeSession とは異なり、StatefulKnowledgeSession では以下のようなメソッドを使って処理を行います。

  • insert でデータを挿入
  • fireAllRules でルールエンジン処理を実行
  • dispose でセッションを終了
order_discount_stateful.groovy
・・・
def session = base.newStatefulKnowledgeSession()

def orders = [
    ・・・
]

orders.each {
    //データの挿入
    session.insert(it)
}

//ルールエンジン処理の実行
session.fireAllRules()
//セッションの終了
session.dispose()

println "--------------------"
・・・

今回用意した .drl ファイルは状態を保持する必要の無いものなので、実行結果は StatelessKnowledgeSession と同じです。

実行例
> groovy order_discount_stateful.groovy
2千円引 : order5
1千円引 : order4
1千円引 : order3
5百円引 : order2
--------------------
order1 : 2000
order2 : 4500
order3 : 9000
order4 : 12000
order5 : 16000

(追記)drools.halt() を使った処理の終了

条件に合致した全てのルールが優先度順に適用されるのを防ぐのに、drools.halt() を使ってルールエンジン処理を終了するのも有効な手段です。

今回のケースに適用すると以下のようになり、DRL ファイルがシンプルになります。

order_discount_halt.drl(halt 利用版)
package fits.sample

import java.math.BigDecimal

dialect "mvel"

rule "5千円以上で5百円引"
    no-loop
    when
        $order : Order(subTotalPrice >= 5000)
    then
        System.out.printf("5百円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(500)
        //ルールエンジンの処理を終了
        drools.halt()
end

rule "1万円以上で千円引"
    no-loop
    salience 10

    when
        $order : Order(subTotalPrice >= 10000)
    then
        System.out.printf("1千円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(1000)
        //ルールエンジンの処理を終了
        drools.halt()
end

rule "1万5千円以上で2千円引"
    no-loop
    salience 20

    when
        $order : Order(subTotalPrice >= 15000)
    then
        System.out.printf("2千円引 : %s \n", $order.name)
        $order.discountPrice = new BigDecimal(2000)
        //ルールエンジンの処理を終了
        drools.halt()
end

ただし、1つのルールを適用すると処理が終了するため、データを 1件ずつ実行する必要があります。

order_discount_halt.groovy
package fits.sample
・・・
builder.add(ResourceFactory.newClassPathResource("order_discount_halt.drl", getClass()), ResourceType.DRL)
・・・
def session = base.newStatelessKnowledgeSession()

def orders = [
    new Order(name: "order1", subTotalPrice: new BigDecimal(2000)),
    ・・・
]

orders.each {
    // 1件ずつ実行
    session.execute(it)
}
・・・
実行例
> groovy order_discount_halt.groovy
5百円引 : order2
1千円引 : order3
1千円引 : order4
2千円引 : order5
--------------------
order1 : 2000
order2 : 4500
order3 : 9000
order4 : 12000
order5 : 16000

*1:複合イベント処理 Complex Event Processing