Drools によるビジネスルールの定義と実行
このガイドでは、Quarkus アプリケーションが Drools を活用してインテリジェントな自動化を実現し、Drools ルールエンジンで強化する方法を紹介します。
要件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.9
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
はじめに
Drools は、インテリジェントな自動化と意思決定管理に焦点を当てた一連のプロジェクトであり、特に前向き連鎖と後向き連鎖の推論ベースのルールエンジン、DMN 意思決定エンジン、およびその他のプロジェクトを提供しています。ルールエンジンは、人工知能において、人間の専門家の意思決定能力をエミュレートするコンピューターシステムであるエキスパートシステムを作成するための基本的な設定要素です。詳細は、 Drools の Web サイト を参照してください。
Drools では、2 つの異なるプログラミングスタイルでルールを定義できます。1 つは、ビジネスルールのリポジトリーとして機能する KieBase と、それらに対して実行時データを保存および評価する KieSession の概念に基づいた、より伝統的なスタイルです。もう 1 つは、ルールセットとそれらのルールが照合されるファクトの両方の定義をカプセル化する単一の抽象化としてルールユニットを使用するスタイルです。
これら両方のスタイルは Drools Quarkus エクステンションで完全にサポートされており、このドキュメントでは両方の使用方法を説明し、それぞれの長所と短所を概説します。
従来の Drools プログラミングモデルと Quarkus の統合
この最初の例では、従来の Drools スタイルを使用してルールセットを定義する方法と、Quarkus を介して REST エンドポイント内でその評価を公開する方法を示します。
このサンプルプロジェクトのドメインモデルは、ローン申請と
public class LoanApplication {
private String id;
private Applicant applicant;
private int amount;
private int deposit;
private boolean approved = false;
public LoanApplication(String id, Applicant applicant, int amount, int deposit) {
this.id = id;
this.applicant = applicant;
this.amount = amount;
this.deposit = deposit;
}
}
そしてその申請者の 2 つのクラスのみで構成されています。
public class Applicant {
private String name;
private int age;
public Applicant(String name, int age) {
this.name = name;
this.age = age;
}
}
ルールセットは、アプリケーションを承認または拒否するビジネス上の決定と、承認されたすべてのアプリケーションをリストに収集する最後の 1 つのルールで設定されます。
global Integer maxAmount;
global java.util.List approvedApplications;
rule LargeDepositApprove when
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount )
then
modify($l) { setApproved(true) }; // loan is approved
end
rule LargeDepositReject when
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount > maxAmount )
then
modify($l) { setApproved(false) }; // loan is rejected
end
// ... more loans approval/rejections business rules ...
rule CollectApprovedApplication when
$l: LoanApplication( approved )
then
approvedApplications.add($l); // collect all approved loan applications
end
目標として、マイクロサービスに、これらのルールの評価を配置し、Quarkus で開発した REST エンドポイントに公開します。これには、プロジェクトの依存関係に Drools Quarkus エクステンションを追加するだけで十分です。
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
この時点で、以前に定義したルールを評価する KieSession への参照を取得し、次のように REST エンドポイントで使用することができます。
@Path("/find-approved")
public class FindApprovedLoansEndpoint {
@Inject
KieRuntimeBuilder kieRuntimeBuilder;
@POST()
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public List<LoanApplication> executeQuery(LoanAppDto loanAppDto) {
KieSession session = kieRuntimeBuilder.newKieSession();
List<LoanApplication> approvedApplications = new ArrayList<>();
session.setGlobal("approvedApplications", approvedApplications);
session.setGlobal("maxAmount", loanAppDto.getMaxAmount());
loanAppDto.getLoanApplications().forEach(session::insert);
session.fireAllRules();
session.dispose();
return approvedApplications;
}
}
ここで、 KieRuntimeBuilder
インターフェイスの実装は Drools エクステンションによって自動的に生成され、挿入可能になり、Drools プロジェクトで定義された任意の KieBases および KieSessions のインスタンスを 1 つのステートメントで取得できるようになります。
ここで、 LoanAppDto
は、同じ KieSession に複数のローン申請を送信する場合に使用される単純な POJO です。
public class LoanAppDto {
private int maxAmount;
private List<LoanApplication> loanApplications;
public int getMaxAmount() {
return maxAmount;
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
public List<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(List<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
}
たとえば、ローン申請のセットでそのエンドポイントを呼び出そうとすると
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d
'{"maxAmount":5000,"loanApplications":[
{"id":"ABC10001","amount":2000,"deposit":1000,"applicant":{"age":45,"name":"John"}},
{"id":"ABC10002","amount":5000,"deposit":100,"applicant":{"age":25,"name":"Paul"}},
{"id":"ABC10015","amount":1000,"deposit":100,"applicant":{"age":12,"name":"George"}}
]}'
http://localhost:8080/find-approved
ルールエンジンは、事前に設定したビジネスルールに基づいてそれらを評価し、その中で承認可能なものを 1 つだけ返します。
[{"id":"ABC10001","applicant":{"name":"John","age":45},"amount":2000,"deposit":1000,"approved":true}]
ルールユニットプログラミングモデルへの移行
ルールユニットは、ルールセットとそれらのルールが照合されるファクトの両方をカプセル化する、Drools で導入された新しい概念です。これには、ファクトが挿入されるソースを定義するデータソースと呼ばれる 2 番目の抽象化が付属しており、実際には型付きエントリーポイントとして機能します。データソースには 2 つの種類があります。
-
DataStream: 追加のみのデータソースです。
-
サブスクライバーは新しいメッセージ (および過去のメッセージ) のみを受信します
-
更新/削除できません
-
“リアクティブストリーム“ の用語では、ストリームはホット/コールドでも問題ない
-
-
DataStore: 変更可能なデータのデータソース
-
サブスクライバーはファクトハンドルを操作してデータストアを操作できる。
-
quarkus アプリケーションでルールユニットを使用するには、2 番目の依存関係を追加する必要があります。
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
本質的に、ルールユニットは、評価されるファクトの定義と、それらを評価するルールのセットという、厳密に関連した 2 つの部分で構成されます。最初の部分は POJO で実装され、ローンの例では次のようになります。
package org.loans;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class LoanUnit implements RuleUnitData {
private int maxAmount;
private DataStore<LoanApplication> loanApplications;
public LoanUnit() {
this(DataSource.createStore(), 0);
}
public LoanUnit(DataStore<LoanApplication> loanApplications, int maxAmount) {
this.loanApplications = loanApplications;
this.maxAmount = maxAmount;
}
public DataStore<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(DataStore<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
public int getMaxAmount() {
return maxAmount;
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
}
ここでは、JSONリクエストのマーシャリング/アンマーシャリングに導入した LoanAppDto
を使用するのではなく、ルールユニットを表すクラスを直接バインドしています。2つの重要な違いは、このクラスが RuleUnitData
インターフェイスを実装していること、そして、承認対象のローン申請を含む単純な List の代わりに DataStore を使用していることです。前者は、このクラスがルールユニット定義の一部であることをエンジンに通知するための単なるマーカーインターフェイスです。 DataStore
は、ルールエンジンが変更に応じて適切に反応し、新しいルールを実行したり、他のルールをトリガーしたりできるように使用する必要があります。この例では、ルールの結果としてローン申請の approved
プロパティーが変更されます。逆に、 maxAmount
の値はルールユニットの設定パラメーターと見なすことができ、そのままにできます。これは、ルール評価中にグローバルと同じセマンティクスで自動的に処理され、最初の例のように JSONリクエストによって渡された値から自動的に設定されるため、引き続きルールで使用できます。
ルールユニットの 2 番目の部分は、このユニットに属するルールを含む drl ファイルです。
package org.loans;
unit LoanUnit; // no need to using globals, all variables and facts are stored in the rule unit
rule LargeDepositApprove when
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ] // oopath style
then
modify($l) { setApproved(true) };
end
rule LargeDepositReject when
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount > maxAmount ]
then
modify($l) { setApproved(false) };
end
// ... more loans approval/rejections business rules ...
// approved loan applications are now retrieved through a query
query FindApproved
$l: /loanApplications[ approved ]
end
このルールファイルでは、同じルールユニットに属していることを示すために、 RuleUnitData
インターフェイスを実装する Java クラスと同じパッケージと、同じ名前のユニットを宣言する必要があります。
このファイルも、新しい OOPath 表記法を使用して書き直されています。予想どおり、ここではデータソースが型指定されたエントリーポイントとして機能し、oopath 式の名前はルートとして設定され、制約は次の例のように角括弧で囲まれています。
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ]
または、データソースの名前をエントリーポイントとして指定する古い DRL 構文を使用することもできますが、この場合、エンジンがデータソースのタイプから推測できる場合でも、一致するオブジェクトのタイプを再度指定する必要があるという欠点があります。
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount ) from entry-point loanApplications
最後に、承認されたローン申請をすべてグローバルな List
に収集する最後のルールが、単にそれらを取得するクエリーに置き換えられたことに注意してください。ルールユニットを使用する利点の 1 つとして、計算のコンテキスト、つまりルール評価に入力として渡される事実を明確に定義できる点が挙げられます。同様に、クエリーはこの評価によって想定される出力を定義します。
この計算境界の明確な定義により、Drools はクエリーを実行してその結果を返すクラスを自動的に生成し、ルールユニットを入力として受け取り、それを以前のクエリー実行者に渡して出力として返す REST エンドポイントも生成できます。
必要な数のクエリーを作成でき、クエリーごとに、キャメルケース (FindApproved
など) からダッシュ区切り (find-approved
など) に変換された同じクエリー名を持つ異なる REST エンドポイントが生成されます。
より包括的な例
このより包括的で完全な例では、基本的な Quarkus アプリケーションにいくつかの簡単なルールを追加して、ホームオートメーション設定の状態で発生する可能性のある問題を推測します。
Drools ルールユニットとルールを DRL 形式で定義します。
Quarkus アプリケーションで使用するために、Rule Unit を標準の Quarkus CDI Bean に接続します (たとえば、Kafka からの MQTT メッセージの接続など)。
要件
このガイドを完了するには、以下が必要です。
-
15 分弱
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.3 以降
-
Docker
-
link:{https://quarkus.io/guides/building-native-image} [GraalVM がインストールされている] (ネイティブモードで実行する場合)
Maven プロジェクトの作成
まず、新しい Quarkus プロジェクトが必要です。 新しい Quarkus プロジェクトを作成するには、{https://quarkus.io/guides/maven-tooling} [Quarkus と Maven ガイド] を参照してください。
Quarkus プロジェクトを設定したら、次の依存関係を pom.xml
に追加することで、Drools Quarkus エクステンションをプロジェクトに追加できます。
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
アプリケーションの記述
アプリケーションドメインモデルから始めましょう。
このアプリケーションの目標は、ホームオートメーション設定の状態に関して発生する可能性のある問題を推測することであるため、家の中のセンサー、デバイス、その他のものの状態を表すために必要なドメインモデルを作成します。
軽量のデバイスドメインモデル:
package org.drools.quarkus.quickstart.test.model;
public class Light {
private final String name;
private Boolean powered;
public Light(String name, Boolean powered) {
this.name = name;
this.powered = powered;
}
// getters, setters, etc.
}
CCTV セキュリティーカメラドメインモデル:
package org.drools.quarkus.quickstart.test.model;
public class CCTV {
private final String name;
private Boolean powered;
public CCTV(String name, Boolean powered) {
this.name = name;
this.powered = powered;
}
// getters, setters, etc.
}
WiFi ドメインモデルで検出されたスマートフォン:
package org.drools.quarkus.quickstart.test.model;
public class Smartphone {
private final String name;
public Smartphone(String name) {
this.name = name;
}
// getters, setters, etc.
}
問題が発生する可能性のあると検出された情報を保持するアラートクラス:
package org.drools.quarkus.quickstart.test.model;
public class Alert {
private final String notification;
public Alert(String notification) {
this.notification = notification;
}
// getters, setters, etc.
}
次に、Quarkus プロジェクトの src/main/resources/org/drools/quarkus/quickstart/test
フォルダー内にルールファイル rules.drl
を作成します。
package org.drools.quarkus.quickstart.test;
unit HomeRuleUnitData;
import org.drools.quarkus.quickstart.test.model.*;
rule "No lights on while outside"
when
$l: /lights[ powered == true ];
not( /smartphones );
then
alerts.add(new Alert("You might have forgot one light powered on: " + $l.getName()));
end
query "AllAlerts"
$a: /alerts;
end
rule "No camera when present at home"
when
accumulate( $s: /smartphones ; $count : count($s) ; $count >= 1 );
$l: /cctvs[ powered == true ];
then
alerts.add(new Alert("One CCTV is still operating: " + $l.getName()));
end
このファイルには、家の全体的な状態が不適切であるかどうかを判断し、必要なアラートをトリガーするためのサンプルルールがいくつかあります。
ルールユニットは、Drools 8 で導入された中心的なパラダイムであり、ユーザーがルールのセットとそれらのルールが照合される事実をカプセル化するのに役立ちます。詳細は、 Drools ドキュメント を参照してください。
ファクトは、タイプセーフなエントリーポイントである DataStore
に挿入されます。すべてが機能するようにするには、RuleUnit と DataStore の両方を定義する必要があります。
package org.drools.quarkus.quickstart.test;
import org.drools.quarkus.quickstart.test.model.Alert;
import org.drools.quarkus.quickstart.test.model.CCTV;
import org.drools.quarkus.quickstart.test.model.Light;
import org.drools.quarkus.quickstart.test.model.Smartphone;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class HomeRuleUnitData implements RuleUnitData {
private final DataStore<Light> lights;
private final DataStore<CCTV> cctvs;
private final DataStore<Smartphone> smartphones;
private final DataStore<Alert> alerts = DataSource.createStore();
public HomeRuleUnitData() {
this(DataSource.createStore(), DataSource.createStore(), DataSource.createStore());
}
public HomeRuleUnitData(DataStore<Light> lights, DataStore<CCTV> cctvs, DataStore<Smartphone> smartphones) {
this.lights = lights;
this.cctvs = cctvs;
this.smartphones = smartphones;
}
public DataStore<Light> getLights() {
return lights;
}
public DataStore<CCTV> getCctvs() {
return cctvs;
}
public DataStore<Smartphone> getSmartphones() {
return smartphones;
}
public DataStore<Alert> getAlerts() {
return alerts;
}
}
アプリケーションのテスト
特定のシナリオセットに応じて、ルールユニットと定義されたルールの動作を確認するの標準の Quarkus および JUnit テストを作成できます。
package org.drools.quarkus.quickstart.test;
@QuarkusTest
public class RuntimeTest {
@Inject
RuleUnit<HomeRuleUnitData> ruleUnit;
@Test
public void testRuleOutside() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", true));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isNotEmpty().anyMatch(kv -> kv.containsValue(new Alert("You might have forgot one light powered on: living room")));
}
@Test
public void testRuleInside() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", true));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
homeUnitData.getCctvs().add(new CCTV("security camera 1", false));
homeUnitData.getCctvs().add(new CCTV("security camera 2", true));
homeUnitData.getSmartphones().add(new Smartphone("John Doe's phone"));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isNotEmpty().anyMatch(kv -> kv.containsValue(new Alert("One CCTV is still operating: security camera 2")));
}
@Test
public void testNoAlerts() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", false));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
homeUnitData.getCctvs().add(new CCTV("security camera 1", true));
homeUnitData.getCctvs().add(new CCTV("security camera 2", true));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isEmpty();
}
}
Quarkus CDI Bean を使用したルールユニットの接続
これで、ルールユニットを標準の Quarkus CDI Bean に接続して、Quarkus アプリケーションで一般的に使用できるようになりました。
たとえば、これは後で適切な Quarkus エクステンションを使用して、Kafka 経由で MQTT を通じてデバイスステータスレポートを送信する場合に役立つ可能性があります。
次のようにして、ルールユニット API の使用を抽象化するためのシンプルな CDI Bean を作成します。
package org.drools.quarkus.quickstart.test;
@ApplicationScoped
public class HomeAlertsBean {
@Inject
RuleUnit<HomeRuleUnitData> ruleUnit;
public Collection<Alert> computeAlerts(Collection<Light> lights, Collection<CCTV> cameras, Collection<Smartphone> phones) {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
lights.forEach(homeUnitData.getLights()::add);
cameras.forEach(homeUnitData.getCctvs()::add);
phones.forEach(homeUnitData.getSmartphones()::add);
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
var queryResults = unitInstance.executeQuery("AllAlerts");
List<Alert> results = queryResults.stream()
.flatMap(m -> m.values().stream()
.filter(Alert.class::isInstance)
.map(Alert.class::cast))
.collect(Collectors.toList());
return results;
}
}
同じテストシナリオは、この CDI Bean を使用して適切にリファクタリングできます。
package org.drools.quarkus.quickstart.test;
@QuarkusTest
public class BeanTest {
@Inject
HomeAlertsBean alerts;
@Test
public void testRuleOutside() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", true), new Light("bedroom", false), new Light("bathroom", false)),
Collections.emptyList(),
Collections.emptyList());
assertThat(computeAlerts).isNotEmpty().contains(new Alert("You might have forgot one light powered on: living room"));
}
@Test
public void testRuleInside() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", true), new Light("bedroom", false), new Light("bathroom", false)),
List.of(new CCTV("security camera 1", false), new CCTV("security camera 2", true)),
List.of(new Smartphone("John Doe's phone")));
assertThat(computeAlerts).isNotEmpty().contains(new Alert("One CCTV is still operating: security camera 2"));
}
@Test
public void testNoAlerts() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", false), new Light("bedroom", false), new Light("bathroom", false)),
List.of(new CCTV("security camera 1", true), new CCTV("security camera 2", true)),
Collections.emptyList());
assertThat(computeAlerts).isEmpty();
}
}