Groovy で JBoss Drools を使う - ルールエンジン機能
JBoss Drools はルールエンジン(Drools Expert)の機能と CEP(Drools Fusion)*1 の機能を兼ね備えており、なかなか興味深いツールです。(さらに BPMN2 等も処理できる)
というわけで今回は Drools のルールエンジン機能を Groovy で簡単に試してみました。(CEP 機能は次回 id:fits:20120105)
- Drools 5.4.0 beta1
- Groovy 1.8.5 (java 1.7.0_02 64bit)
サンプルソースは 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 の基本的な処理は以下のような手順で実装します。
- KnowledgeBuilderFactory から KnowledgeBuilder 生成
- KnowledgeBuilder にリソース(ルール定義)追加
- KnowledgeBaseFactory から KnowledgeBase 生成
- KnowledgeBase に KnowledgeBuilder から取得した KnowledgePackages を設定
- KnowledgeBase から StatefulKnowledgeSession や StatelessKnowledgeSession を生成
- 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