日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

使用Drools和JPA實現(xiàn)持續(xù)的實時數(shù)據(jù)分析

 LibraryPKU 2014-09-20

企業(yè)開發(fā)人員按照管理復(fù)雜工作流、業(yè)務(wù)規(guī)則和業(yè)務(wù)智能來分派任務(wù),這樣可以快速實現(xiàn)企業(yè)平臺的價值,該平臺集成了工作流引擎、企業(yè)服務(wù)總線 (ESB) 和規(guī)則引擎。迄今為止,這個出色的平臺已經(jīng)被 IBM WebSphere? Process Server/WebSphere Enterprise Service Bus(參見 參考資料)和 Oracle SOA Suite 之類的商用產(chǎn)品填滿了。來自 Boss Community 的 Drools 5 是一種開源替代方案,它通過一組統(tǒng)一的 API 和一個共享的、有狀態(tài)的知識會話來無縫集成 jBPM 工作流引擎和規(guī)則引擎。

面向初學(xué)者

請注意,本文假設(shè)您熟悉 Spring 平臺、Java Persistence API 和 Drools 的基本原理。請參閱 參考資料,了解有關(guān)這些主題的更多介紹性文章。

Drools 5 的 Business Logic 集成平臺主要包括 Drools Expert 和 Drools Fusion,這兩項共同組成了平臺的規(guī)則引擎和用于復(fù)雜事件處理/時態(tài)推理的基礎(chǔ)架構(gòu)。本文的樣例應(yīng)用程序是根據(jù)這些核心特性構(gòu)建的。請參閱 參考資料,了解有關(guān) Drools 5 中其他可用程序包的更多信息。

Drools 5 中的 POJO

傳 統(tǒng)的 Java 對象 (POJO) 是在 Spring 框架中首次以引人注目的方式實現(xiàn)的。POJO 以及依賴注入 (DI) 和面向方面的編程 (AOP) 共同標志著向簡單性的回歸,這種簡單性有效地促進 Spring 成為開發(fā) Web 應(yīng)用程序的一種行業(yè)標準。POJO 的采用已經(jīng)從 Spring 流向 EJB 3.0 和 JPA,然后再流向 XML-to-Java 綁定技術(shù)(比如 JAXB 和 XStream)。最近,POJO 已通過 Hibernate Search 集成到了全文搜索引擎 Lucene 中(參閱 參考資料)。

如今,由于這些增加的改進,應(yīng)用程序的 POJO 數(shù)據(jù)模型可以在多個層上進行傳播,并直接通過 Web 頁面或 SOAP/REST Web 服務(wù)端點進行公開。作為一種編程模型,POJO 既經(jīng)濟高效又屬于非侵入性的,這為開發(fā)人員在簡化企業(yè)架構(gòu)時節(jié)約了不少時間。

現(xiàn)在,Drools 5 通過允許直接將 POJO 作為事實 (fact) 直接插入知識會話(knowledge session)中,或是插入一個稱為 “工作內(nèi)存” 的規(guī)則引擎中,將 POJO 編程簡單性應(yīng)用于下一個級別。本文介紹了一種既經(jīng)濟高效又屬于非侵入性的方法,這種方法將 JPA 實體作為 Drools 工作內(nèi)存中的事實來進行操作。持續(xù)的實時數(shù)據(jù)分析從來不會這么簡單。

Drools 編程挑戰(zhàn)

許多醫(yī)療服務(wù)提供商使用案例管理系統(tǒng)作為跟蹤醫(yī)療記錄(比如護理、處方和評估)的一種經(jīng)濟高效的方法。我們的示例程序(基于這樣一種系統(tǒng))具有以下流程和需求:

  • 案例在系統(tǒng)內(nèi)的所有臨床醫(yī)生之間循環(huán)。
  • 臨床醫(yī)生每周至少負責(zé)一個評估任務(wù),或?qū)⑼ㄖl(fā)送給臨床醫(yī)生的監(jiān)管員。
  • 系統(tǒng)給臨床醫(yī)生自動安排評估任務(wù)。
  • 如果案例未評估超過 30 天,則向案例組中的所有臨床醫(yī)生發(fā)送提醒。
  • 如果沒有響應(yīng),系統(tǒng)會采取系統(tǒng)的業(yè)務(wù)規(guī)則定義的措施,比如通知出現(xiàn)問題的臨床醫(yī)生組并提出另一項計劃。

為 該用例選擇一個業(yè)務(wù)流程管理 (BPM) 工作流和規(guī)則引擎是有一定道理的:系統(tǒng)使用數(shù)據(jù)剖析/分析規(guī)則(已在上述列表中用斜體字標出),將每個案例用作在 jBPM 中長期運行的一個流程/工作流,而且我們可以使用一個 Drools Planner 來滿足自動安排的需求。出于本文的目的,我們將只關(guān)注程序的業(yè)務(wù)規(guī)則。我們還要介紹的是系統(tǒng)需求,在滿足規(guī)則條件時立即實時生成提醒和通知。因此這是一個 持續(xù)的實時數(shù)據(jù)分析用例。

清單 1 顯示了在我們的系統(tǒng)中聲明的三個實體類:MemberCase、ClinicianCaseSupervision

清單 1. 實體類
@Entity
@EntityListeners({DefaultWorkingMemoryPartitionEntityListener.class})
public class MemberCase implements Serializable 
{
  private Long id; // pk
  private Date startDtm;
  private Date endDtm;
  private Member member; // not null (memberId)
  private List<CaseSupervision> caseSupervisions = new ArrayList<CaseSupervision>();
  //...
}
 
@Entity
@EntityListeners({DefaultWorkingMemoryPartitionEntityListener.class})
public class Clinician implements Serializable 
{ 
  private Long id; // pk
  private Boolean active;
  private List<CaseSupervision> caseSupervisions = new ArrayList<CaseSupervision>();
	//...
}

@Entity
@EntityListeners({SupervisionStreamWorkingMemoryPartitionEntityListener.class})
public class CaseSupervision implements Serializable 
{ 
  private Long id; // pk
  private Date entryDtm;
  private MemberCase memberCase;
  private Clinician clinician;
  //...
}

MemberCase 中的每個實例代表一個病歷。Clinician 代表機構(gòu)中的臨床醫(yī)生。臨床醫(yī)生每次進行案例評估時會產(chǎn)生一個 CaseSupervision 記錄。同時,這三個實體是將要定義的業(yè)務(wù)規(guī)則中的事實類型。還要注意的是,上述 CaseSupervision 被聲明為 Drools 中的一個事件類型。

從應(yīng)用程序的角度來看,我們可以從系統(tǒng)的任何地方、在不同的屏幕上、在不同的工作流中修改這三種類型的實體。我們甚至可以使用 Spring Batch 這樣的工具來批量更新實體。然而,出于本例的考慮,讓我們假設(shè)將只通過 JPA 持久上下文來更新實體。

注意,樣例應(yīng)用程序是一個 Spring-Drools 集成,它使用 Maven 來完成構(gòu)建。本文稍后將考慮一些配置細節(jié),但是您可以隨時 下載源 zip。現(xiàn)在,讓我們考慮一些使用 Drools 5 的概念特性。

事實和 FactHandle

規(guī)則引擎的一般概念是:事實 (fact) 是規(guī)則所依賴的數(shù)據(jù)對象。在 Drools 中,事實是從應(yīng)用程序獲得且斷言為引擎的工作內(nèi)存的任意 Java bean。或者說,就像在 JBoss Drools 參考手冊 中撰寫的那樣:

規(guī) 則引擎根本沒有 “克隆” 事實,它是一天結(jié)束時的所有引用/指針 (pointer)。事實是您的應(yīng)用程序數(shù)據(jù)。沒有 getter 和 setter 的 Strings 和其他類不是有效的 Fact,不能和 Field Constraints 一起使用,F(xiàn)ield Constraints 依靠 getter 和 setter 的 JavaBean 標準與對象進行交互。

除非您在規(guī)則之上已經(jīng)指定了關(guān)鍵字 no-looplock-on-active,否則在工作內(nèi)存中發(fā)生事實更改時,不能在任何時候重新評估 Drools 規(guī)則引擎中的規(guī)則。您還可以使用 @PropertyReactive@watch 注釋來指定事實屬性,Drools 應(yīng)該監(jiān)視這些屬性來應(yīng)對更改。Drools 會忽略對事實的其他所有屬性的更新。

出于實際維護的目的,有三種方法來安全更新 Drools 工作內(nèi)存中的事實

  1. 在 Drools 語法中,右邊 (RHS) 是規(guī)則的操作/結(jié)果部分,您可以在 modify 塊內(nèi)對其進行更新。將一個事實更改為將要激活的規(guī)則的結(jié)果時,會使用這個方法。
  2. 從外部通過 Java 類中的 FactHandle;用于由應(yīng)用程序 Java 代碼執(zhí)行的事實更改。
  3. Fact 類實現(xiàn) PropertyChangeSupport,就像 JavaBeans 規(guī)范定義的那樣;使用此方法將 Drools 注冊為 Fact 對象的 PropertyChangeListener。

作為安靜的觀察者,我們的規(guī)則不會更新 Drools 工作內(nèi)存中的任何 JPA 實體事實;相反,它們會將邏輯事實生成為推理結(jié)果。(參見下列的 清單 6。)但是,更新規(guī)則中的 JPA 實體時需要特別注意,因為更新的實體可能處于分離狀態(tài),或者沒有事務(wù)或只讀事務(wù)與當(dāng)前線程有關(guān)聯(lián)。因此,對實體所做的更改將不會保存到數(shù)據(jù)庫中。

盡管事實對象是因為引用而被傳遞,Drools(與 JPA/Hibernate 不同)不能跟蹤超出規(guī)則之外的事實更改。您可以通過使用 FactHandle 來通知 Drools 有關(guān)在應(yīng)用程序 Java 代碼中所做的事實更改,從而避免產(chǎn)生不一致的規(guī)則推理結(jié)果。Drools 接著會對規(guī)則進行適當(dāng)?shù)闹匦略u估。FactHandle 是表示您在工作內(nèi)存中斷言的事實對象的標記。這就是您希望修改或取消一個事實時與工作內(nèi)存的正常交互方式。在樣例應(yīng)用程序(清單 2清單 3)中,我們使用 FactHandle 來操作工作內(nèi)存中的實體事實。

您可以通過實現(xiàn) PropertyChangeSupport(它捕獲了對 bean 的屬性所做的每一項更改)來解決 Drools 無法跟蹤事實更改的問題。但是,請記住,這也是隨后由于執(zhí)行頻繁的規(guī)則重新評估而需要解決的性能問題。

使用 JPA 實體作為事實

您可以通過 POJO 事實將 JPA 實體作為域數(shù)據(jù)對象插入到 Drools 的工作內(nèi)存中。這樣做可以讓您避免對 Value Object/DTO 層以及 JPA 實體和 DTO 之間的相應(yīng)轉(zhuǎn)換層進行數(shù)據(jù)建模。

將 實體用作事實會簡化應(yīng)用程序代碼,您必須額外注意 “實體-生命周期” 階段。實體事實應(yīng)當(dāng)保存為受管(持久)狀態(tài)或分離狀態(tài)。永遠不要將臨時的實體插入到 Drools 工作內(nèi)存中,因為它們還未保存到數(shù)據(jù)庫中。同樣,應(yīng)當(dāng)從工作內(nèi)存中收回已刪除的實體。否則應(yīng)用程序數(shù)據(jù)庫和規(guī)則引擎的工作內(nèi)存會不同步。

因此,這會帶來一些嚴重的問題:我們?nèi)绾尾拍苡行ㄖ?guī)則引擎有關(guān)通過 FactHandle 在應(yīng)用程序代碼中所做的實體更改?

命令式(Imperative)編程與 AOP 的比較

如果想通過命令式編程的方式來應(yīng)對這個挑戰(zhàn),我們需要結(jié)束在緊鄰相應(yīng) JPA API 方法的知識會話上調(diào)用 insert()、update()retract() 方法。這種方法應(yīng)該是 Drools API 的一種入侵性用法,而且應(yīng)將 spaghetti 代碼留在應(yīng)用程序中。更糟糕的是,JPA 中已更新的(臟)實體在讀取/寫入事務(wù)結(jié)束時會自動與數(shù)據(jù)庫同步運行,未對持久上下文進行任何顯式調(diào)用。我們?nèi)绾尾拍軘r截這些更改并通知給 Drools?另一個選擇是,在單獨的進程中輪詢實體更改,就像典型的商業(yè)智能 (BI) 工具所做的那樣,這會使核心業(yè)務(wù)功能保持干凈,但實現(xiàn)此操作很困難,成本較高,結(jié)果也不會是即時的。

JPA EntityListener 是一種 AOP 攔截器,非常適合我們的用例。在 清單 2 中,我們將定義兩個 EntityListener,它們會攔截對應(yīng)用程序中三種類型的實體所做的所有更改。這種方法使得 JPA 中實體 的生命周期與其在 Drools 中的生命周期不斷地保持同步。

在 “實體-生命周期” 回調(diào)方法中,我們?yōu)榻o定的實體實例查找一個 FactHandle,然后根據(jù) JPA 生命周期階段通過返回的 FactHandle 來更新或收回事實。如果 FactHandle 缺失,則會將實體作為新事實 插入到工作內(nèi)存中,以實現(xiàn)事實更新或持久保存。由于工作內(nèi)存中不存在實體,因此在調(diào)用 JPA 刪除時也沒有必要將其從工作內(nèi)存中刪除。清單 2 中所示的兩個 JPA EntityListener 用于工作內(nèi)存中的兩個不同的入口點,或者分區(qū)。第一個入口點在 MemberCaseClinician 之間共享,第二個入口點用于 CaseSupervision 事件類型。

清單 2. EntityListeners
@Configurable
public class DefaultWorkingMemoryPartitionEntityListener 
{
  @Value("#{ksession}") //unable to make @Configurable with compile time weaving work here
  private StatefulKnowledgeSession ksession;   
   
  @PostPersist
  @PostUpdate
  public void updateFact(Object entity)
  {       
    FactHandle factHandle = getKsession().getFactHandle(entity);
    if(factHandle == null)
      getKsession().insert(entity);
    else
      getKsession().update(factHandle, entity);
  }        
		   
  @PostRemove
  public void retractFact(Object entity)
  {
    FactHandle factHandle = getKsession().getFactHandle(entity);
    if(factHandle != null)
      getKsession().retract(factHandle);
  }
 
  public StatefulKnowledgeSession getKsession() 
  {
    if(ksession != null)
    {
      return ksession;
    }
    else
    {
      // a workaround for @Configurable
      setKsession(ApplicationContextProvider.getApplicationContext()
        .getBean("ksession", StatefulKnowledgeSession.class));
      return ksession;
    }
  }
  //...
}
 
@Configurable
public class SupervisionStreamWorkingMemoryPartitionEntityListener
{ 
  @Value("#{ksession}")  
  private StatefulKnowledgeSession ksession;   
	
  @PostPersist 
  // CaseSupervision is an immutable event, 
  // thus we don’t provide @PostUpdate and @PostRemove implementations.
  public void insertFact(Object entity)
  {   
    WorkingMemoryEntryPoint entryPoint = getKsession()
      .getWorkingMemoryEntryPoint("SupervisionStream");
    entryPoint.insert(entity);
  }        
  //...
}

就像 AOP 一樣,清單 2 中的 EntityListener 方法使系統(tǒng)的核心業(yè)務(wù)邏輯保持干凈。注意,這種方法需要一個或多個 Drools 全局知識會話來注入到兩個 EntityListener 中。在本文的后面部分中,我們將一個知識會話聲明為一個單態(tài)(singleton)的 Spring bean。

提示:共享的全局知識會話

從本質(zhì)上來說,共享的全局知識會話使得 EntityListener 方法適合于系統(tǒng)范圍、BI 數(shù)據(jù)分析和分析需求。它同樣不適合特定于用戶流程以及在線購物系統(tǒng)之類的規(guī)則執(zhí)行,在這些系統(tǒng)中,通常會動態(tài)生成知識會話來處理用戶指定的數(shù)據(jù),然后將其解決。

初始化工作內(nèi)存

在啟動應(yīng)用程序后,三種實體類型的所有現(xiàn)有記錄都將從數(shù)據(jù)庫預(yù)加載到用于規(guī)則執(zhí)行的工作內(nèi)存中,如 清單 3 所示。從那時起,會向工作內(nèi)存通知通過兩個 EntityListener 對實體所做的任何更改。

清單 3. 初始化工作內(nèi)存并運行 Drools 查詢
@Service("droolsService")
@Lazy(false)
@Transactional
public class DroolsServiceImpl 
{
  @Value("#{droolsServiceUtil}")
  private DroolsServiceUtil droolsServiceUtil;
    
  @PostConstruct
  public void launchRules()
  {
    droolsServiceUtil.initializeKnowledgeSession();
    droolsServiceUtil.fireRulesUtilHalt();    
  }
   
  public Collection<TransientReminder> findCaseReminders()
  {
    return droolsServiceUtil.droolsQuery("CaseReminderQuery", 
      "caseReminder", TransientReminder.class, null);
  }
   
  public Collection<TransientReminder> findClinicianReminders()
  {
    return droolsServiceUtil.droolsQuery("ClinicianReminderQuery", 
      "clinicianReminder", TransientReminder.class, null);
  }
}  
 
@Service
public class DroolsServiceUtil
{
  @Value("#{ksession}")
  private StatefulKnowledgeSession ksession;
            
  @Async
  public void fireRulesUtilHalt()
  {
    try{
      getKsession().fireUntilHalt(); 
    }catch(ConsequenceException e) 
    {
      throw e;
    }
  }
   
  public void initializeKnowledgeSession()
  {  
    getKsession().setGlobal("droolsServiceUtil", this);
    syncFactsWithDatabase();
  }

  @Transactional //a transaction-scoped persistence context
  public void syncFactsWithDatabase()
  {
    synchronized(ksession)
    {       
      // Reset all the facts in the working memory
      Collection<FactHandle> factHandles = getKsession().getFactHandles(
        new ObjectFilter(){public boolean accept(Object object)
        {
          if(object instanceof MemberCase)
            return true;
          return false;
        }
      });
      for(FactHandle factHandle : factHandles)
      {
        getKsession().retract(factHandle);
      }

      factHandles = getKsession().getFactHandles(
        new ObjectFilter(){public boolean accept(Object object)
        {
          if(object instanceof Clinician)
            return true;
          return false;
        }
      });
      for(FactHandle factHandle : factHandles)
      {
        getKsession().retract(factHandle);
      }           

      WorkingMemoryEntryPoint entryPoint = getKsession()
        .getWorkingMemoryEntryPoint("SupervisionStream");
      factHandles = entryPoint.getFactHandles();
      for(FactHandle factHandle : factHandles)
      {
        entryPoint.retract(factHandle);
      }               

      List<Command> commands = new ArrayList<Command>();
      commands.add(CommandFactory.newInsertElements(getMemberCaseService().findAll()));
      getKsession().execute(CommandFactory.newBatchExecution(commands));

      commands = new ArrayList<Command>();
      commands.add(CommandFactory.newInsertElements(getClinicianService().findAll()));
      getKsession().execute(CommandFactory.newBatchExecution(commands));    
	 
      for(CaseSupervision caseSupervision : getCaseSupervisionService().findAll())
      {
        entryPoint.insert(caseSupervision);
      }  
           
    }
  }
 
  public <T> Collection<T> droolsQuery(String query, String variable, 
    Class<T> c, Object... args)
  {
    synchronized(ksession)
    {       
      Collection<T> results = new ArrayList<T>();
      QueryResults qResults = getKsession().getQueryResults(query, args);  
      for(QueryResultsRow qrr : qResults)
      {
        T result = (T) qrr.get("$"+variable);
        results.add(result);
      }       
      return results;
    }
  }
}

有關(guān) fireAllRules() 的注意事項

請注意,在 清單 3 中,我們擁有在各個 EntityListener 的回調(diào)方法中調(diào)用 fireAllRules() 的選項。在一個急切加載 (eager-loaded) 的 Spring bean 的 “@PostConstruct” 方法中,我只調(diào)用了一次 fireUntilHalt() 方法就簡化了這一步驟。fireUtilHalt 方法應(yīng)該在單獨的線程中調(diào)用一次(查看 Spring 的 @Async 注釋),隨后不斷觸發(fā)規(guī)則激活,直至調(diào)用停止。如果沒有需要觸發(fā)的激活,fireUtilHalt 會等待將激活添加到一個激活議程組或規(guī)則流組中。

我可以選擇在應(yīng)用程序的 Spring XML 配置文件(如下所示)中觸發(fā)規(guī)則,甚至是啟動流程。然而,我在嘗試配置 fireUntilHalt() 方法時檢測到了一個可能的線程處理問題。在對懶惰式加載 (lazy-loading) 實體關(guān)系進行規(guī)則評估期間,結(jié)果是一個 “數(shù)據(jù)庫連接已關(guān)閉的錯誤”(參見高級主題)。

Spring-Drools 集成

現(xiàn)在,讓我們花一些時間來看看 Spring-Drools 集成的一些配置細節(jié)。清單 4 是應(yīng)用程序的 Maven pom.xml 的一個代碼段,包括用于 Drools 內(nèi)核、Drools 編譯器和 Drools Spring 集成包的依賴關(guān)系:

清單 4. 部分 Maven pom.xml
<dependency>
  <groupId>org.drools</groupId>
  <artifactId>drools-core</artifactId>
  <version>5.4.0.Final</version>
  <type>jar</type>
</dependency>               
<dependency>
  <groupId>org.drools</groupId>
  <artifactId>drools-compiler</artifactId>
  <version>5.4.0.Final</version>
  <type>jar</type>
</dependency>
<dependency> 
  <groupId>org.drools</groupId> 
  <artifactId>drools-spring</artifactId> 
  <version>5.4.0.Final</version> 
  <type>jar</type> 
  <exclusions>
    <!-- The dependency pom includes spring and hibernate dependencies by mistake. -->	
  </exclusions>
</dependency>

身份與等同性的比較

清單 5 中,我將一個全局有狀態(tài)知識會話配置為一個單態(tài)的 Spring bean。(一個無狀態(tài)知識會話不會充當(dāng)一個持續(xù)時間很長的會話,因為它在迭代調(diào)用期間沒有保持其狀態(tài)。)清單 5 中需要注意的一個重要設(shè)置是 <drools:assert-behavior mode="EQUALITY" />。

在 JPA/Hibernate 中,托管實體將與身份(identity) 進行比較,而分離的實體將與等同性(equality) 進行比較。插入到有狀態(tài)實體會話中的實體快速從 JPA 角度分離。因為與單態(tài)的有狀態(tài)知識會話的生命期相比,一個事務(wù)范圍的持續(xù)上下文,甚至是一個 “擴展的” 或 “流范圍的” 持續(xù)上下文(參見 參考資料)是臨時的。每次通過不同的持續(xù)上下文對象取得的同一個實體是不同的 Java 對象。默認情況下,Drools 使用的是身份比較。因此,當(dāng)通過 ksession.getFactHandle(entity) 在工作內(nèi)存中的現(xiàn)有實體事實上查看 FactHandle 時,Drools 很可能不會找到匹配項。為了與分離的實體相匹配,我們必須在配置文件中選擇 EQUALITY。

清單 5. 部分 Spring applicationContext.xml
<drools:kbase id="kbase">
  <drools:resources>
    <drools:resource  type="DRL" source="classpath:drools/rules.drl" />
  </drools:resources>
  <drools:configuration>
    <drools:mbeans enabled="true" />
    <drools:event-processing-mode mode="STREAM" />
    <drools:assert-behavior mode="EQUALITY" />
  </drools:configuration>
</drools:kbase>
<drools:ksession id="ksession" type="stateful" name="ksession" kbase="kbase" />

看看應(yīng)用程序源代碼,了解更完整的配置細節(jié)。

Drools 規(guī)則

清單 6 定義了兩個復(fù)雜的事件處理 (CEP) 規(guī)則。除了類似 JPA 的兩個事實類型之外,MemberCaseClinicianCaseSupervision 實體類被聲明為一個事件。臨床醫(yī)生執(zhí)行的每個案例評估任務(wù)都會生成一個 CaseSupervision 記錄。創(chuàng)建記錄之后,不可能一直不斷地對其進行更改。

清單 6 中的 Case Supervision 規(guī)則的條件可用來測試在過去的 30 天內(nèi)案例上是否已經(jīng)存在案例監(jiān)督。如果沒有,規(guī)則的結(jié)果/措施部分會生成一個 TransientReminder 事實(在 清單 7 中定義),并從邏輯上將事實插入工作內(nèi)存。Clinician Supervision 規(guī)則指示,臨床醫(yī)生在過去的七天內(nèi)應(yīng)當(dāng)已經(jīng)完成至少一個案例監(jiān)督;如果沒有完成,規(guī)則的結(jié)果/措施部分會生成一個類似的 TransientReminder 事實,并從邏輯上插入到工作內(nèi)存中。

清單 6. 案例監(jiān)督規(guī)則
package ibm.developerworks.article.drools;

import ibm.developerworks.article.drools.service.*
import ibm.developerworks.article.drools.domain.*
 
global DroolsServiceUtil droolsServiceUtil;

declare Today
  @role(event)
  @expires(24h)
end

declare CaseSupervision
  @role(event)
  @timestamp(entryDtm)
end

rule "Set Today"
  timer (cron: 0 0 0 * * ?)
  salience 99999  // optional
  no-loop
  when
  then
    insert(new Today()); 
end

rule "Case Supervision"
  dialect "mvel"
  when
    $today : Today()
    $memberCase : MemberCase(endDtm == null, startDtm before[30d] $today)
    not CaseSupervision(memberCase == $ memberCase) 
      over window:time(30d) from entry-point SupervisionStream
    then
      insertLogical(new TransientReminder($memberCase, (Clinician)null, 
        "CaseReminder", "No supervision on the case in last 30 days."));
end
 
query "CaseReminderQuery"
  $caseReminder : TransientReminder(reminderTypeCd == "CaseReminder")
end
 
rule "Clinician Supervision"
  dialect "mvel"
  when
    $clinician : Clinician()
    not CaseSupervision(clinician == $clinician) 
      over window:time(7d) from entry-point SupervisionStream
  then
    insertLogical(new TransientReminder((MemberCase)null, $clinician, 
      "ClinicianReminder", "Clinician completed no evaluation in last 7 days."));
end
 
query "ClinicianReminderQuery"
  $clinicianReminder : TransientReminder(reminderTypeCd == "ClinicianReminder")
end

請注意,清單 7 中所示的 TransientReminder 事實不是一個 JPA 實體,而是一個常規(guī)的 POJO。

清單 7. TransientReminder
public class TransientReminder implements Comparable, Serializable
{			
  private MemberCase memberCase;
  private Clinician clinician;
  private String reminderTypeCd;
  private String description;

  public String toString() 
  {
    return ReflectionToStringBuilder.toString(this);
  }

  public boolean equals(Object pObject) 
  {
    return EqualsBuilder.reflectionEquals(this, pObject);
  }

  public int compareTo(Object pObject) 
  {
    return CompareToBuilder.reflectionCompare(this, pObject);
  }

  public int hashCode() 
  {
    return HashCodeBuilder.reflectionHashCode(this);
  } 	
}

事實與事件的比較

事件是使用 @timestamp@duration@expires 之類的時態(tài)元數(shù)據(jù)進行裝飾的事實。事實和事件之間最重要的區(qū)別是,事件在 Drools 上下文中是不可變的。如果一個事件受更改的制約,那么更改(描述為 “事件數(shù)據(jù)補充”)不應(yīng)當(dāng)影響規(guī)則執(zhí)行的結(jié)果。這就是我們在 CaseSupervisionEntityListener 中只監(jiān)視 @PostPersist 實體生命周期階段的原因(參見 清單 2)。

Drools 對 Sliding Windows 協(xié)議的支持使得事件對時態(tài)推理特別有用。滑動窗口 是為感興趣的事件制定作用域的一種方式,就好像它們屬于一個不斷移動的窗口一樣。兩種最常見的滑動窗口實現(xiàn)是基于時間的窗口和基于長度的窗口。

清單 6 中所示的樣例規(guī)則中,over window:time(30d) 建議,過去 30 天中創(chuàng)建的 CaseSupervision 事件由規(guī)則引擎進行評估。一旦過了 30 天,不可變的事件將永遠不會再次進入到窗口中,而且 Drools 將自動從工作內(nèi)存中收回這些事件,并對相應(yīng)的規(guī)則進行重新評估。由于事件是不可變的,所以 Drools 會自動管理事件生命周期。因而事件比事實更具有內(nèi)存效率。(但是,請注意,您必須在 Drools-Spring 配置中將事件處理模式設(shè)置為 STREAM;否則滑動窗口之類的時態(tài)操作符會停止工作。)

使用已聲明的類型

清單 6 中需要注意的其他一些事項包括:MemberCase 事實(不屬于事件類型)也針對事件約束進行了評估,就像我們只評估之間 30 多天內(nèi)創(chuàng)建的案例一樣。某個案例可能今天已經(jīng)存在 29 天了,但明天就是 30 天了,這就意味著必須在每一天的一開始就對 Case Supervision 規(guī)則進行重新評估。不幸的是,Drools 并不提供 “今天” 變化的量。因此,作為一個變通方案,我添加了一個名為 Today 的事件類型;這是一個 Drools 聲明類型,或是一個用規(guī)則語言(而不是用 Java 代碼)聲明的數(shù)據(jù)構(gòu)造。

這種特殊的事件類型根本不聲明任何顯式屬性,除了一個隱式的 @timestamp 元數(shù)據(jù),后者是在 Today 事件斷言到工作內(nèi)存中時進行自動填充。另一個元數(shù)據(jù) @expires(24h) 指定,某個 Today 事件會在斷言后的 24 小時內(nèi)到期。

要想在每天的一開始重設(shè) Today,還需要在 Set Today 規(guī)則之上添加了一個 timer。先激活這個規(guī)則,然后就會在每天的一開始觸發(fā)該規(guī)則來插入一個新鮮的 Today 事件,該事件將取代剛剛期滿的事件。隨后,新鮮的 Today 事件會觸發(fā) Case Supervision 規(guī)則的重估。還要注意的是,如果規(guī)則的條件中沒有出現(xiàn)事實更改,那么計時器本身無法觸發(fā)規(guī)則的重估。計時器也不能重新評估函數(shù)或內(nèi)聯(lián)的 eval,因為 Drools 將這些構(gòu)造函數(shù)的返回結(jié)果作為時間常量,并緩存它們的值。

何時使用事實與事件的比較

了解事實和事件之間的區(qū)別有助于我們輕松決定何時使用每種類型:

  • 在某個時間點或某段持續(xù)時間內(nèi),當(dāng)數(shù)據(jù)表示系統(tǒng)狀態(tài)的不可變快照時,應(yīng)該將事件 用于場景,該事件是時間敏感的,并且會很快到期,或可以預(yù)計數(shù)據(jù)量會快速且持續(xù)增長。
  • 在數(shù)據(jù)對業(yè)務(wù)域至關(guān)重要的地方,以及數(shù)據(jù)將體驗正在進行的更改并且這些更改要求持續(xù)重估規(guī)則的時候,應(yīng)該將事實 用于場景。

Drools 查詢

下一個步驟是提取規(guī)則執(zhí)行結(jié)果,這通過查詢工作內(nèi)存中的事實來完成。(一種替代方法是通過調(diào)用規(guī)則語法右手邊的 global 上的方法,讓規(guī)則引擎將結(jié)果傳遞給應(yīng)用程序。)在這個例子中,事實斷言和規(guī)則觸發(fā)都立即發(fā)生,沒有任何延遲,這就確保了我們在 清單 6 中的查詢會返回實時的報告。因為 TransientReminder 事實是通過邏輯方式斷言的,所以在它們的條件再無法得到滿足時,規(guī)則引擎會自動從工作內(nèi)存中收回它們。

可以說提醒 是在早上由一個規(guī)則引擎在特定的案例上生成的。隨后,我們在 Java 代碼中執(zhí)行查詢 “CaseReminderQuery”,如 清單 3 所示,因此會返回一個提醒,并向系統(tǒng)中的所有臨床醫(yī)生顯示該提醒。如果在下午某個臨床醫(yī)生完成了案例上的一個評估,并生成了一個新的案例監(jiān)督記錄,該事件會打破用于提醒 事實的條件。Drools 隨后會自動收回它。我們要確定該提醒事實消失了,方法是在完成案例評估之后立即運行相同的查詢。邏輯斷言可以使推理結(jié)果保持最新,而且規(guī)則引擎運行在內(nèi)存效率模式之下,這與事件的行為非常相像。

邏輯事實計數(shù)器

請注意,每一個以邏輯方式斷言的事實都附帶一個計數(shù)器,該計數(shù)器在每次斷言一個同等 事實時都增加。如果不再保持反復(fù)斷言同等 事實的眾多規(guī)則中的某個規(guī)則,那么用于邏輯事實的計數(shù)器就會減少。當(dāng)計數(shù)器達到 0 時,就會自動收回該事實。

現(xiàn)場查詢 更是錦上添花。讓一個現(xiàn)場查詢處于打開狀態(tài),這會創(chuàng)建一個查詢結(jié)果視圖,并發(fā)布給定視圖內(nèi)容的更改事件。這意味著現(xiàn)場查詢恰好需要運行一次,由此產(chǎn)生的結(jié)果視圖會使用由規(guī)則引擎發(fā)布的正在進行的更改來實現(xiàn)自動更新。

到目前為止,您可能已經(jīng)發(fā)現(xiàn),只需一點點有關(guān) Drools、JPA 和 Spring 的背景知識,就可以輕松實現(xiàn)一個持續(xù)的實時數(shù)據(jù)分析應(yīng)用程序。我們將通過一些將改進我們的案例管理解決方案的高級編程步驟來結(jié)束本文。

高級 Drools 編程

管理關(guān)系

FactHandle 的一個有趣的約束條件是它只與當(dāng)前事實相關(guān)聯(lián),與事實的嵌套關(guān)系沒有關(guān)聯(lián)。Drools 會通過其在 getKsession().update(factHandle, memberCase) 中的 FactHandle,了解對 MemberCaseid(盡管這永遠不可能發(fā)生,因為主鍵是不可變的)、startDtmendDtm 所做的更改。然而,在調(diào)用同一個方法時,不會通知 Drools 有關(guān)對 membercaseSupervisions 屬性所做的更改。

同樣,系統(tǒng)不會通知 JPA 中的 EntityListener 通知有關(guān)一對多和多對多關(guān)系的更改。這是因為外鍵位于相關(guān) 表或鏈接 表中。

為了根據(jù)更新的事實與這些關(guān)系建立連接,我們可以構(gòu)建遞歸邏輯,獲取每個嵌套關(guān)系的 FactHandle。一個更好的解決方案是將 EntityListener 放置在與規(guī)則條件有關(guān)中的所有實體上(包括鏈接表)。我們使用 MemberCaseSupervision 來完成此操作,其中更改由每個實體自己的 EntityListenerFactHandle 來處理的(參見 清單 2清單 3)。

規(guī)則評估期間的實體懶惰式加載

除非我們已經(jīng)指定一個知識庫分區(qū)(也就是說,可以執(zhí)行并行處理),否則不會在調(diào)用 ksession.insert()、ksession.update()ksession.retract() 的同一個線程中對規(guī)則進行評估。清單 2清單 3 中的事實斷言都發(fā)生在事務(wù)上下文中,在該上下文中,事務(wù)范圍的 JPA 持久上下文(Hibernate 會話)是可用的。這就允許規(guī)則引擎跨懶惰式加載實體關(guān)系進行評估。如果啟用了一個知識庫分區(qū),則必須將實體關(guān)系配置為急切加載,以避免產(chǎn)生 JPA LazyInitializationException。

啟用事務(wù)

默認情況下,Drools 不支持事務(wù),因為它在工作內(nèi)存中不保存任何歷史快照。這對我們的 EntityListener 來說是一個問題,因為生命周期回調(diào)方法是在數(shù)據(jù)庫刷新之后、但在事務(wù)提交之前調(diào)用的。如果事務(wù)被回滾又會怎么樣呢?如果那樣的話,JPA 持久上下文中的實體將變?yōu)榉蛛x的實體,且與數(shù)據(jù)庫表中的行不一致,而且工作內(nèi)存中的行也是如此。規(guī)則引擎推理結(jié)果將不再是可信的。

啟用事務(wù)通過確保工作內(nèi)存中的數(shù)據(jù)庫和應(yīng)用程序數(shù)據(jù)庫始終同步,且規(guī)則推理結(jié)果始終準確,使我們的案例管理系統(tǒng)具有防彈功能。在 Drools 中,適當(dāng)應(yīng)用 JPA 和 JTA 實現(xiàn)以及類路徑中的一個 “drools-jpa-persistence” 包,可以配置一個 JPAKnowledgeService(參閱 參考資料)來創(chuàng)建我們的有狀態(tài)知識會話。具有流程實例、變量和事實對象的整個有狀態(tài)知識會話可以映射為表 “SessionInfoThe”(將 ksessionId 作為主鍵)中行的一個二進制列。

當(dāng) 我們通過注釋或 XML 在應(yīng)用程序中指定事務(wù)邊界時,應(yīng)用程序啟動的事務(wù)會傳播到規(guī)則引擎。無論什么時候發(fā)生事務(wù)回滾,有狀態(tài)知識會話都會恢復(fù)到數(shù)據(jù)庫中保存的以前的狀態(tài)。這維 護了應(yīng)用程序數(shù)據(jù)庫和 Drools 數(shù)據(jù)庫之間的一致性和集成。當(dāng)同時從多個 JTA 事務(wù)中進行訪問時,內(nèi)存中的單個有狀態(tài)知識會話應(yīng)當(dāng)像 REPEATABLE READ 一樣運轉(zhuǎn);否則,單個 SessionInfo 實體實例可能具有一些從不同事務(wù)所做的混合狀態(tài)更改,這會打破事務(wù)劃分。請注意,自編寫 REPEATABLE READ 起,就不能確定它是否能夠通過 drools-jpa-persistence 包的事務(wù)管理器來實現(xiàn)。

集群

如果應(yīng)用程序要在集群環(huán)境下運行,前面描述的方法很快就會失敗。每個嵌入式規(guī)則引擎的實例都會接收同一個節(jié)點上發(fā)生的實體事件,這會導(dǎo)致不同節(jié)點上的工作內(nèi)存不同步。我們可以使用一個通用的遠程 Drools 服務(wù)器(參閱 參考資料)來解決這個問題。不同節(jié)點上的實體實例會通過 REST/SOAP Web 服務(wù)通信向集中式 Drools 服務(wù)器發(fā)布其所有的事件。隨后應(yīng)用程序可以從 Drools 服務(wù)器訂閱推理結(jié)果。請注意,Drools 服務(wù)器中 SOAP 的 Apache CXF 實現(xiàn)目前不支持 ws-transaction。考慮到針對該真實用例概述的義務(wù)性事務(wù)需求,我希望很快就能提供這方面的支持。

結(jié)束語

在本文中,您有機會匯總一些您已經(jīng)了解的有關(guān)在 Spring 和 JPA 中進行 POJO 編程的知識,同時本文還匯總了一些在 Drools 5 中可用的新特性。我已經(jīng)演示了如何巧妙使用 EntityListener(一個全局 Drools 會話)和 fireUtilHalt() 方法,用它們來開發(fā)一個基于 POJO 的持續(xù)實時數(shù)據(jù)分析應(yīng)用程序。您已經(jīng)了解了核心的 Drools 概念,比如致力于 “事實與事件的比較” 之類的主題,還了解了如何編寫邏輯斷言,以及更高級的主題和使用,比如,事務(wù)管理和將 Drools 實現(xiàn)擴展到一個集群環(huán)境中。請參閱 應(yīng)用程序源代碼,了解有關(guān) Drools 5 的更多信息。

下載

描述名字大小
本文的樣例代碼j-drools5-src.zip5KB

參考資料

學(xué)習(xí)

獲得產(chǎn)品和技術(shù)

  • 下載 Drools 5:本文中使用了 Drools Expert 和 Drools Fusion,它們可以實現(xiàn)規(guī)則引擎和 CEP 框架。

討論

  • 加入 developerWorks 中文社區(qū)。查看開發(fā)人員推動的博客、論壇、討論組和維基,并與其他 developerWorks 用戶交流。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多