PolarSPARC |
Java Rules Engine - Drools :: Part 4
Bhaskar S | 07/17/2021 |
Overview
In Part 1, we provided an overview of Drools and its core components.
In Part 2, we demonstrated two examples - one to isolate rules in different KieBases and the other to showcase a pseudo real-world scenario.
In Part 3, we demonstrated two examples - one to showcase the situation of hierarchical decisions and the other to execute rules in parallel and prove that each KieSession is isolated from the other.
In this part, we will demonstrate how one could store (and manage) rules in a relational database and load them at runtime for execution.
Hands-on with Drools
In the Seventh application, we demonstrate the following simple business logic - get quotes from a supplier for a product for two days and create a price delta object for comparison. If the price delta percent is greater than 10%, we display a message. If the price delta percent is less than 15%, we do nothing. The rules are stored in the Postgres database table called RULES_TBL and loaded into Drools at runtime.
Seventh Application
To setup the directory structure for the database server, execute the following command:
$ mkdir -p $HOME/Downloads/postgres
To download the required docker image for the PostgreSQL database server, execute the following command:
$ docker pull postgres:13.2
To start the PostgreSQL database server on the localhost, oen a terminal window and execute the following command:
$ docker run -d --rm --name postgres-13.2 -e POSTGRES_USER=polarsparc -e POSTGRES_PASSWORD=polarsparc\$123 -p 5432:5432 -v $HOME/Downloads/postgres:/var/lib/postgresql/data postgres:13.2
To create a database called my_test_db, execute the following command in the terminal:
$ docker exec -it postgres-13.2 sh
The prompt changes to # and continue to execute the following command:
# psql -U polarsparc
The prompt changes to polarsparc=# and continue to execute the following commands:
polarsparc=# CREATE DATABASE my_test_db;
polarsparc=# GRANT ALL PRIVILEGES ON DATABASE my_test_db TO polarsparc;
polarsparc=# \q
The prompt changes to # and continue to execute the following command:
# psql my_test_db -U polarsparc
The prompt changes to my_test_db=> and continue to execute the following commands:
my_test_db=> CREATE TABLE RULES_TBL (RULE_ID SERIAL PRIMARY KEY, RULE_TXT TEXT NOT NULL);
my_test_db=> \q
The prompt changes to # and continue to execute the following command:
# exit
To setup the Java directory structure for the Seventh application, execute the following commands:
$ cd $HOME/java/Drools
$ mkdir -p $HOME/java/Drools/Seventh
$ mkdir -p Seventh/src/main/java Seventh/src/main/resources Seventh/target
$ cd $HOME/java/Drools/Seventh
The following is the listing for the Maven project file pom.xml that will be used:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.polarsparc</groupId> <artifactId>Drools</artifactId> <version>1.0</version> </parent> <artifactId>Seventh</artifactId> <version>1.0</version> <name>Seventh</name> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> </dependencies> </project>
The contents of the simplelogger.properties located in the directory src/main/resources will be identical to the one from the First application listed in Part 1 and hence we will not show them here again.
The following is the listing for the Spring Boot application properties file application.properties located in the directory src/main/resources:
# ### Spring Boot Application properties # spring.main.banner-mode=off spring.datasource.url=jdbc:postgresql://localhost:5432/my_test_db spring.datasource.username=polarsparc spring.datasource.password=polarsparc$123
The following is the Drools rules set, shown as an illustration with four sections, each of which is stored in the database column rule_txt of the table rules_tbl:
Using any database client, execute the following INSERT statements to store the above 4 sections in the database table rules_tbl:
INSERT INTO RULES_TBL(RULE_TXT) VALUES (E'package com.polarsparc.seventh;\n\nimport com.polarsparc.seventh.model.*;\nimport org.slf4j.Logger;\n\nglobal org.slf4j.Logger log;');
INSERT INTO RULES_TBL(RULE_TXT) VALUES (E'rule "SelectQuote"\n when\n $s1: Quote(day == 0, supplier == "S1", $p1: price)\n $s2: Quote(day == 1, supplier == "S1", $p2: price)\n then\n Delta delta = new Delta("S1", $s1.getProduct(), ($p2 - $p1)/$p1);\n\n log.info("{}: Delta {}", drools.getRule().getName(), delta);\n\n delete($s1);\n delete($s2);\n insert(delta);\nend');
INSERT INTO RULES_TBL(RULE_TXT) VALUES (E'rule "DeltaCheckOne"\n when\n $del: Delta(supplier == "S1", delta > 0.10)\n then\n log.info("{}: Delta {} is greater than 10%", drools.getRule().getName(), $del);\n\n delete($del);\nend');
INSERT INTO RULES_TBL(RULE_TXT) VALUES (E'rule "DeltaCheckTwo"\n when\n $del: Delta(supplier == "S1", delta > 0.15)\n then\n log.info("{}: Delta {} is greater than 15%", drools.getRule().getName(), $del);\n\n delete($del);\nend');
The following is the Java POJO that encapsulates the quote details:
/* * Name: Quote * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @AllArgsConstructor @ToString public class Quote { private int day; private String supplier; private String product; private double price; }
The following is the Java POJO that encapsulates the price delta details:
/* * Name: Delta * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @AllArgsConstructor @ToString public class Delta { private String supplier; private String product; private double delta; }
The following is the Java POJO that encapsulates the rule details from the database table rules_tbl:
/* * Name: Rule * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.model; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class Rule { private int ruleId; private String ruleTxt; }
The following is the Java DAO interface for accessing the rules stored in the database:
/* * Name: RuleDAO * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.repository; import com.polarsparc.seventh.model.Rule; import java.util.List; public interface RuleDAO { List<Rule> findRules(List<String> ids); }
The following is the Java class that maps each row from the database query of the table rules_tbl into a Rule POJO instance:
/* * Name: RuleRowMapper * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.repository; import com.polarsparc.seventh.model.Rule; import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; public class RuleRowMapper implements RowMapper<Rule> { @Override public Rule mapRow(ResultSet rs, int no) throws SQLException { Rule rule = new Rule(); rule.setRuleId(rs.getInt("RULE_ID")); rule.setRuleTxt(rs.getString("RULE_TXT")); return rule; } }
The following is the Java class that implements the Java DAO interface and represents the Spring Boot repository bean:
/* * Name: RuleRepository * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.repository; import com.polarsparc.seventh.model.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; @Repository public class RuleRepository implements RuleDAO { final String QUERY_BY_IDS = "SELECT rule_id, rule_txt FROM rules_tbl WHERE rule_id IN (%s) ORDER BY rule_id"; private JdbcTemplate jdbcTemplate; @Autowired public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<Rule> findRules(List<String> ids) { return jdbcTemplate.query(String.format(QUERY_BY_IDS, String.join(",", ids)), new RuleRowMapper()); } }
The following is the Java utility that creates the desired Drools container by loading the rules from the database:
/* * Name: DroolsUtil * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh.util; import com.polarsparc.seventh.model.Rule; import lombok.extern.slf4j.Slf4j; import org.kie.api.KieServices; import org.kie.api.builder.*; import org.kie.api.io.KieResources; import org.kie.api.io.Resource; import org.kie.api.io.ResourceType; import org.kie.api.runtime.KieContainer; import org.springframework.beans.factory.BeanCreationException; import java.io.ByteArrayInputStream; import java.util.List; @Slf4j public final class DroolsUtil { private final static String VIRTUAL_DRL_FILE = "com/polarsparc/seventh/seventh.drl"; private final static KieServices services = KieServices.Factory.get(); private DroolsUtil() {} public static KieContainer prepareKieContainer(String tag, List<Rule> rules) { ReleaseId releaseId = services.newReleaseId("com.polarsparc.seventh", "seventh", tag); KieFileSystem fileSystem = services.newKieFileSystem(); StringBuilder sb = new StringBuilder(); rules.forEach(rule -> sb.append(rule.getRuleTxt()).append("\n\n")); log.info("---> Drools Rules Set:\n\n{}", sb); KieResources resources = services.getResources(); Resource drlResource = resources.newInputStreamResource(new ByteArrayInputStream(sb.toString().getBytes())) .setResourceType(ResourceType.DRL); fileSystem.write(VIRTUAL_DRL_FILE, drlResource); fileSystem.write(drlResource); fileSystem.generateAndWritePomXML(releaseId); KieBuilder builder = services.newKieBuilder(fileSystem); Results results = builder.buildAll().getResults(); if (results.hasMessages(Message.Level.ERROR)) { throw new BeanCreationException("Error building rules: " + results.getMessages()); } KieModule module = builder.getKieModule(); return services.newKieContainer(module.getReleaseId()); } }
The following is the main Spring Boot application to test the Drools rules engine:
/* * Name: SeventhApplication * Author: Bhaskar S * Date: 07/17/2021 * Blog: https://www.polarsparc.com */ package com.polarsparc.seventh; import com.polarsparc.seventh.model.Quote; import com.polarsparc.seventh.model.Rule; import com.polarsparc.seventh.repository.RuleDAO; import com.polarsparc.seventh.repository.RuleRepository; import com.polarsparc.seventh.util.DroolsUtil; import lombok.extern.slf4j.Slf4j; import org.kie.api.runtime.KieContainer; import org.kie.api.runtime.KieSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.util.Arrays; import java.util.List; @SpringBootApplication @Slf4j public class SeventhApplication implements ApplicationRunner { private RuleDAO repository; @Autowired public void setRepository(RuleRepository repository) { this.repository = repository; } public static void main(String[] args) { SpringApplication.run(SeventhApplication.class, args); } @Override public void run(ApplicationArguments args) { // Case - 1 List<Rule> rules = repository.findRules(Arrays.asList("1", "2", "3")); KieContainer container = DroolsUtil.prepareKieContainer("1.0", rules); log.info("Part 1 - ReleaseId: {}", container.getReleaseId()); KieSession session = container.newKieSession(); session.setGlobal("log", log); session.insert(new Quote(0,"S1", "P1", 9.99)); session.insert(new Quote(1,"S1", "P1", 10.99)); session.fireAllRules(); session.dispose(); log.info("Part 1 --- Done !!!"); // Case - 2 rules = repository.findRules(Arrays.asList("1", "2", "4")); container = DroolsUtil.prepareKieContainer("1.1", rules); log.info("Part 2 - ReleaseId: {}", container.getReleaseId()); session = container.newKieSession(); session.setGlobal("log", log); session.insert(new Quote(0,"S1", "P1", 9.99)); session.insert(new Quote(1,"S1", "P1", 10.99)); session.fireAllRules(); session.dispose(); log.info("Part 2 --- Done !!!"); } }
To execute the code from Listing.43, open a terminal window and run the following commands:
$ cd $HOME/java/Drools/Seventh
$ mvn spring-boot:run
The following could be the typical output:
2021-07-17 14:04:58:810 [main] INFO com.polarsparc.seventh.SeventhApplication - Starting SeventhApplication using Java 15.0.2 on sringeri with PID 24363 (/home/polarsparc/java/Drools/Seventh/target/classes started by polarsparc in /home/polarsparc/java/Drools/Seventh) 2021-07-17 14:04:58:811 [main] INFO com.polarsparc.seventh.SeventhApplication - No active profile set, falling back to default profiles: default 2021-07-17 14:04:59:335 [main] INFO com.polarsparc.seventh.SeventhApplication - Started SeventhApplication in 0.801 seconds (JVM running for 1.062) 2021-07-17 14:04:59:336 [main] INFO org.springframework.boot.availability.ApplicationAvailabilityBean - Application availability state LivenessState changed to CORRECT 2021-07-17 14:04:59:351 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... 2021-07-17 14:04:59:423 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. 2021-07-17 14:04:59:475 [main] INFO com.polarsparc.seventh.util.DroolsUtil - ---> Drools Rules Set: package com.polarsparc.seventh; import com.polarsparc.seventh.model.*; import org.slf4j.Logger; global org.slf4j.Logger log; rule "SelectQuote" when $s1: Quote(day == 0, supplier == "S1", $p1: price) $s2: Quote(day == 1, supplier == "S1", $p2: price) then Delta delta = new Delta("S1", $s1.getProduct(), ($p2 - $p1)/$p1); log.info("{}: Delta {}", drools.getRule().getName(), delta); delete($s1); delete($s2); insert(delta); end rule "DeltaCheckOne" when $del: Delta(supplier == "S1", delta > 0.10) then log.info("{}: Delta {} is greater than 10%", drools.getRule().getName(), $del); delete($del); end 2021-07-17 14:05:00:181 [main] INFO com.polarsparc.seventh.SeventhApplication - Part 1 - ReleaseId: com.polarsparc.seventh:seventh:1.0 2021-07-17 14:05:00:182 [main] INFO org.drools.compiler.kie.builder.impl.KieContainerImpl - Start creation of KieBase: defaultKieBase 2021-07-17 14:05:00:231 [main] INFO org.drools.compiler.kie.builder.impl.KieContainerImpl - End creation of KieBase: defaultKieBase 2021-07-17 14:05:00:280 [main] INFO com.polarsparc.seventh.SeventhApplication - SelectQuote: Delta Delta(supplier=S1, product=P1, delta=0.10010010010010009) 2021-07-17 14:05:00:283 [main] INFO com.polarsparc.seventh.SeventhApplication - DeltaCheckOne: Delta Delta(supplier=S1, product=P1, delta=0.10010010010010009) is greater than 10% 2021-07-17 14:05:00:283 [main] INFO com.polarsparc.seventh.SeventhApplication - Part 1 --- Done !!! 2021-07-17 14:05:00:284 [main] INFO com.polarsparc.seventh.util.DroolsUtil - ---> Drools Rules Set: package com.polarsparc.seventh; import com.polarsparc.seventh.model.*; import org.slf4j.Logger; global org.slf4j.Logger log; rule "SelectQuote" when $s1: Quote(day == 0, supplier == "S1", $p1: price) $s2: Quote(day == 1, supplier == "S1", $p2: price) then Delta delta = new Delta("S1", $s1.getProduct(), ($p2 - $p1)/$p1); log.info("{}: Delta {}", drools.getRule().getName(), delta); delete($s1); delete($s2); insert(delta); end rule "DeltaCheckTwo" when $del: Delta(supplier == "S1", delta > 0.15) then log.info("{}: Delta {} is greater than 15%", drools.getRule().getName(), $del); delete($del); end 2021-07-17 14:05:00:323 [main] INFO com.polarsparc.seventh.SeventhApplication - Part 2 - ReleaseId: com.polarsparc.seventh:seventh:1.1 2021-07-17 14:05:00:323 [main] INFO org.drools.compiler.kie.builder.impl.KieContainerImpl - Start creation of KieBase: defaultKieBase 2021-07-17 14:05:00:327 [main] INFO org.drools.compiler.kie.builder.impl.KieContainerImpl - End creation of KieBase: defaultKieBase 2021-07-17 14:05:00:331 [main] INFO com.polarsparc.seventh.SeventhApplication - SelectQuote: Delta Delta(supplier=S1, product=P1, delta=0.10010010010010009) 2021-07-17 14:05:00:332 [main] INFO com.polarsparc.seventh.SeventhApplication - Part 2 --- Done !!! 2021-07-17 14:05:00:333 [main] INFO org.springframework.boot.availability.ApplicationAvailabilityBean - Application availability state ReadinessState changed to ACCEPTING_TRAFFIC 2021-07-17 14:05:00:336 [SpringContextShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated... 2021-07-17 14:05:00:340 [SpringContextShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.327 s [INFO] Finished at: 2021-07-17T14:05:00-04:00 [INFO] ------------------------------------------------------------------------
As can be observed from the Output.8 above, we first load the rules 1, 2, 3 and execute the rules. The price delta is greater than 10% and hence we see the message. In the second pass, we load the rules 1, 2, 4 and execute the rules. This time around, we do not see any price delta message.
References
Java Rules Engine - Drools :: Part 3
Java Rules Engine - Drools :: Part 2