Использование Drools и JPA для непрерывного профилирования данных в режиме реального времени

Программирование POJO-объектов JPA в качестве фактов в рабочей памяти Drools 5

Drools можно интегрировать с кодом приложения на основе JPA и Spring ― не прибегая к навязчивому программированию в императивном стиле. Научитесь эффективно внедрять бизнес-требования в свои системы мониторинга реального времени и процесс непрерывного профилирования данных с помощью POJO-объектов. Автор Лю Синьюй делится своим опытом в области технологий персистентности Java™ и бизнес-интеграции, давая практические советы по использованию Drools 5 для создания надежных приложений, эффективно использующих память.

Синьюй Лю, вице-президент по разработке продуктов, eHealthObjects

Сертифицированный архитектор корпоративных продуктов Sun Microsystems, Лю Синьюй (Xinyu Liu) обладает богатым опытом разработки приложений на основе новейших серверных технологий. Он окончил университет им. Джорджа Вашингтона и в настоящее время является вице-президентом по разработке продуктов компании eHealthObjects, специализирующейся на ИТ для здравоохранения и выпускающей инновационные продукты, решения, услуги и платформы, которые можно гладко интегрировать с другими системами, службами и приложениями. Д-р Лю пишет для Java.net, JavaWorld.com и IBM developerWorks на такие темы, как JSF, Spring Security, Hibernate Search, Spring Web Flow, и спецификация сервлетов Servlet 3.0. Он также работает в издательстве Packt Publishing, где аннотировал книги Spring Web Flow 2 Web Development, Grails 1.1 Web Application Development и Application Development for IBM WebSphere Process Server 7 and Enterprise Service Bus 7.



22.02.2013

Корпоративные разработчики, решающие задачи по управлению сложными рабочими потоками, бизнес-правилами и бизнес-анализом, быстро осознали значение корпоративной платформы, объединяющей механизм рабочих потоков, архитектуру Enterprise Service Bus (ESB) и механизм управления правилами. До сих пор она использовалась коммерческими продуктами, такими как IBM WebSphere® Process Server/WebSphere Enterprise Service Bus (см. раздел Ресурсы) и Oracle SOA Suite. Альтернатива с открытым исходным кодом Drools 5 от JBoss Community гладко интегрирует механизм рабочего процесса jBPM с механизмом управления правилами с помощью набора унифицированных API и общего сеанса управления знаниями с отслеживанием состояния.

Для начинающих

Эта статья предполагает, что читатель знаком с платформой Spring, Java Persistence API и основами Drools. Ссылки на статьи по каждой из этих тем приведены в разделе Ресурсы .

Платформа интеграции Drools 5 Business Logic состоит главным образом из компонентов Drools Expert и Drools Fusion, которые вместе составляют механизм управления правилами платформы и инфраструктуру для обработки/временнóго анализа сложных событий. Пример приложения для этой статьи построен на базе этих основных функций. В разделе Ресурсы приведены ссылки на литературу о других пакетах, имеющихся в Drools 5.

POJO-объекты в Drools 5

Объекты Plain old Java (POJO) впервые были эффективно реализованы в среде Spring. Появление POJO-объектов вместе с внедрением зависимостей (Dependency Injection - DI) и аспектно-ориентированным программированием (Aspect-Oriented Programming ― AOP) ознаменовало возвращение к простоте, чему эффективно способствовал отраслевой стандарт разработки Web-приложений Spring. Освоение POJO-объектов перекинулось из Spring в EJB 3.0 и JPA, а оттуда ― в технологии связывания XML-Java, такие как JAXB и XStream. Совсем недавно POJO-объекты интегрированы в механизм полнотекстового поиска Lucene посредством Hibernate Search (см. раздел Ресурсы).

Сегодня в результате всех этих достижений модель данных приложения POJO можно провести через несколько уровней и отобразить непосредственно на Web-страницах или в конечных точках Web-сервисов SOAP/REST. Экономически эффективная и ненавязчивая модель программирования POJO экономит время разработчиков и упрощает корпоративные архитектуры.

Теперь Drools 5 поднял простоту POJO-программирования на новый уровень, позволив программистам внедрять POJO-объекты как факты непосредственно в сеанс управления знаниями или то, что в механизме управления правилами называется «рабочей памятью». Эта статья знакомит читателя с экономически эффективным и ненавязчивым подходом, при котором сущности JPA рассматриваются как факты в рабочей памяти Drools. В результате непрерывное профилирование данных в режиме реального времени предельно упрощается.

Задача для Drools-программирования

Многие медицинские учреждения используют систему управления делами в качестве экономически эффективного способа ведения медицинских записей, таких как истории болезни, рецепты и заключения. В нашем примере программы, основанном на такой системе, предусмотрены следующие операции и требования:

  • дела (истории болезни) циркулируют между всеми врачами в системе;
  • врачи должны давать по крайней мере одно заключение в неделю, иначе руководству направляется уведомление;
  • система автоматически планирует загрузку врачей;
  • если по случаю заболевания не дано заключение в течение 30 дней, то всем врачам, занимающимся данным пациентом, направляется напоминание;
  • если ответа нет, система принимает меры, определенные правилами, такие как уведомление группы врачей о проблеме и предложение другого графика обследования.

Выбирая механизм управления рабочим процессом и бизнес-правилами (business process management - BPM) для этого приложения, следует учесть, что система использует правила профилирования/анализа данных (выделены курсивом в приведенном выше списке), каждый случай заболевания можно рассматривать как продолжительный рабочий процесс в jBPM, и для выполнения требования автоматического планирования можно использовать Drools Planner. Для целей этой статьи мы сосредоточимся только на бизнес-правилах программы. Допустим также, что система требует, чтобы при выполнении условия правила напоминания и уведомления создавались сразу, в режиме реального времени. Таким образом, мы имеем дело с непрерывным профилированием данных в режиме реального времени.

В листинге 1 показаны три класса сущностей, которые объявляются в нашей системе: MemberCase, Clinician и CaseSupervision:

Листинг 1. Классы сущностей
@Entity
@EntityListeners({DefaultWorkingMemoryPartitionEntityListener.class})
public class MemberCase implements Serializable 
{
  private Long id; // pk
  private Date startDtm;
  private Date endDtm;
  private Member member; // не 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 ― это врачи клиники. Запись CaseSupervision создается каждый раз, когда врач дает заключение по случаю заболевания. Вместе эти три сущности составляют типы фактов в определяемых бизнес-правилах. Обратите также внимание, что CaseSupervision объявляется как тип событий в Drools.

С точки зрения приложения сущности трех типов можно изменять в любом месте системы на разных экранах и в разных рабочих процессах. Можно даже использовать такой инструмент, как Spring Batch, для пакетного обновления сущностей. Однако для данного примера предположим, что сущности редактируются исключительно через контекст персистентности JPA.

Отметим, что пример приложения представляет собой интеграцию Spring-Drools с использованием Maven для сборки. Ниже мы рассмотрим некоторые детали конфигурации, но читатель может загрузить zip-архив с исходным кодом в любое время. Пока же рассмотрим некоторые концептуальные особенности работы с Drools 5.

Факт и FactHandle

Общая идея механизма правил заключается в том, что факты ― это объекты данных, к которым применяются правила. В Drools факты представляют собой произвольные bean-компоненты Java, которые берутся из приложения и помещаются в рабочую память механизма. Или, как сказано в справочном руководстве по JBoss Drools:

Механизм правил вовсе не «клонирует» факты, все это просто ссылки/указатели. Факты ― это данные, с которыми работает приложение. Строки и другие классы без геттеров и сеттеров не являются допустимыми фактами и не могут использоваться с Field Constraints, которые для взаимодействия с объектом опираются на стандарт геттеров и сеттеров JavaBean.

Если в начале правила не указать ключевое слово no-loop или lock-on-active, то правила в механизме управления правилами Drools будут переоцениваться каждый раз при изменении факта в рабочей памяти. Можно также использовать аннотации @PropertyReactive и @watch для указания свойств факта, за изменениями которых Drools должен следить. Изменения всех остальных свойств этого факта Drools будет игнорировать.

Существуют три способа безопасного изменения факта в рабочей памяти Drools.

  1. В синтаксисе Drools правая часть выражения (RHS) представляет собой раздел действия/следствия правила, который можно изменять в блоке modify. Этот подход следует использовать при изменении факта в результате применения правила.
  2. Внешне через FactHandle в классе Java; используется для изменения факта в коде Java-приложения.
  3. Когда класс Fact реализует метод PropertyChangeSupport согласно спецификации JavaBeans; используйте его для регистрации Drools в качестве PropertyChangeListener для объекта Fact.

Будучи безмолвными наблюдателями, наши правила не изменяют никаких фактов сущностей JPA в рабочей памяти Drools; вместо этого они генерируют логические факты как результаты причинно-следственных связей. (См. Листинг 6 ниже.) Однако изменение сущностей JPA в правилах требует дополнительного внимания, так как измененные сущности могут находиться в отсоединенном состоянии, или может случиться так, что с текущим потоком не связаны никакие транзакции. В результате изменения сущностей не будут сохранены в базе данных.

Хотя объекты-факты передаются по ссылке, Drools (в отличие от JPA/Hibernate) не способен отслеживать изменения фактов, внесенные вне правил. Несогласованных результатов применения правил можно избежать, используя FactHandle для уведомления Drools об изменениях фактов, внесенных в Java-коде приложения. Тогда Drools надлежащим образом переоценит правила. FactHandle ― это маркер, представляющий установленный объект-факт в рабочей памяти. Это обычный способ взаимодействия с рабочей памятью, когда нужно изменить или удалить факт. В нашем примере приложения (Листинг 2 и Листинг 3) FactHandle используется для манипулирования фактами в рабочей памяти.

Неспособность Drools отслеживать внешние изменения фактов можно обойти, реализовав метод PropertyChangeSupport (который фиксирует все изменения свойств компонентов). Однако имейте в виду, что в этом случае придется решать проблему снижения производительности из-за частой переоценки правил.

Использование в качестве фактов JPA-сущностей

JPA-сущности можно вводить в рабочую память Drools посредством POJO-фактов как объекты данных домена. Это позволит избежать моделирования уровня Value Object/DTO, а также соответствующего преобразования уровней между JPA-сущностями и DTO.

Использование сущностей в качестве фактов упростит код приложения, но за это придется платить дополнительным вниманием к этапам жизненного цикла сущностей. Факты-сущности должны находиться в управляемом (персистентном) или отделенном состоянии. Никогда не вводите в рабочую память Drools сущности, находящиеся в переходном состоянии, так как они еще не сохранены в базе данных Drools. Аналогично, удаленные сущности следует убирать из рабочей памяти. В противном случае нарушится синхронизация между базой данных приложения и рабочей памятью механизма правил.

Но это подводит нас к важному вопросу: как эффективно уведомлять механизм правил об изменениях сущностей, внесенных в коде приложения посредством маркера FactHandle?

Императивное программирование и AOP

Если попытаться решить эту задачу с позиций императивного программирования, мы придем к вызову методов insert(), update() и retract() в сеансе анализа, следующем за соответствующими методами API JPA. Такой подход будет означать навязчивое использование API Drools и оставит в приложении спагетти-код. Еще хуже то, что измененные (черновые) сущности в JPA автоматически синхронизируются с базой данных в конце операции чтения/записи без всякого явного обращения к контексту персистентности. Как перехватить эти изменения и уведомить о них Drools? Другой вариант, опрос изменений сущностей в отдельном процессе, как это делается в типичных инструментах бизнес-анализа (BI), оставляет ключевые бизнес-функции чистыми, но он труден и дорог в реализации, а результаты не будут получаться мгновенно.

Для нашего случая хорошо подходит JPA EntityListener ― своего рода перехватчик AOP. В листинге 2 мы определяем два таких EntityListener, которые перехватывают все изменения, внесенные в сущности трех типов приложения. Этот подход сохраняет жизненный цикл сущности в JPA постоянно синхронизированным с его жизненным циклом в Drools.

В методах обратного вызова в течение жизненного цикла сущности разыскивается маркер FactHandle для данного экземпляра сущности, после чего факт изменяется или удаляется с помощью полученного маркера FactHandle в зависимости от этапа жизненного цикла JPA. Если FactHandle отсутствует, то сущность вводится в рабочую память для изменения или сохранения в качестве нового факта. Так как в рабочей памяти сущности не существует, нет необходимости удалять ее из рабочей памяти при вызове JPA delete. Два перехватчика JPA EntityListener, показанные в листинге 2, предназначены для разных точек входа, или разделов рабочей памяти. Первая точка входа ― общая для MemberCase и Clinician, а вторая предназначена для событий типа CaseSupervision.

Листинг 2. Перехватчики EntityListener
@Configurable
public class DefaultWorkingMemoryPartitionEntityListener 
{
  @Value("#{ksession}") //не удается сделать @Configurable
  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
    {
      // обходной путь для @Configurable
      setKsession(ApplicationContextProvider.getApplicationContext()
        .getBean("ksession", StatefulKnowledgeSession.class));
      return ksession;
    }
  }
  //...
}
 
@Configurable
public class SupervisionStreamWorkingMemoryPartitionEntityListener
{ 
  @Value("#{ksession}")  
  private StatefulKnowledgeSession ksession;   
	
  @PostPersist 
  // CaseSupervision ― неизменяемое событие, 
  // поэтому мы не создаем реализаций @PostUpdate и @PostRemove.
  public void insertFact(Object entity)
  {   
    WorkingMemoryEntryPoint entryPoint = getKsession()
      .getWorkingMemoryEntryPoint("SupervisionStream");
    entryPoint.insert(entity);
  }        
  //...
}

Как и AOP, подход с использованием EntityListener в листинге 2 сохраняет чистоту основной бизнес-логики системы. Обратите внимание, что этот подход требует одного или более глобальных сеансов анализа Drools в двух перехватчиках EntityListener. Позднее мы объявим сеанс анализа в качестве singleton-компонента Spring.

Совет: обобщенные глобальные сеансы анализа

Обобщенный глобальный сеанс анализа (shared global knowledge session), по существу, делает подход с использованием EntityListener подходящим для BI-профилирования данных и анализа по всей системе. Но он не подходит для выполнения специальных процессов и правил, таких как те, что используются в системах онлайн-торговли, где сеансы анализа обычно генерируются динамически для обработки специфических пользовательских данных, а затем удаляются.

Инициализация рабочей памяти

После запуска приложения все существующие записи по трем типам сущностей предварительно загружаются из базы данных в рабочую память для оценки правил, как показано в листинге 3. С этого момента рабочая память уведомляется о любых изменениях, внесенных в сущности, посредством двух перехватчиков EntityListener.

Листинг 3. Инициализация рабочей памяти и выполнение запросов 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 //контекст персистентности транзакции 
  public void syncFactsWithDatabase()
  {
    synchronized(ksession)
    {       
      // сброс всех фактов в рабочей памяти
      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;
    }
  }
}

Примечание по поводу метода fireAllRules()

Обратите внимание, что в листинге 3 можно вызывать метод fireAllRules() в пределах каждого из методов обратного вызова fireAllRules(). Я упростил это, вызвав метод fireUntilHalt() только один раз внутри метода компонента Spring @PostConstruct. Предполагается, что метод fireUtilHalt вызывается один раз в отдельном потоке (извлечение аннотации Spring @Async), после чего продолжает активировать правила, пока не будет остановлен. Если активировать нечего, fireUtilHalt ожидает добавления задания на активацию в группу active-agenda или rule-flow.

Я мог бы выбрать применение правил или даже запуск процессов в XML-файле конфигурации Spring приложения (как показано ниже). Однако я обнаружил возможную проблему управления потоками в методе fireUntilHalt(), когда попытался настроить его. Результатом стало сообщение об ошибке «соединение с базой данных закрыто» во время загрузки отношений сущностей при оценке правил (см. углубленные темы).

Интеграция Spring-Drools

Теперь рассмотрим некоторые детали настройки интеграции Spring-Drools. В листинге представлен фрагмент файла Maven pom.xml приложения, содержащий зависимости для ядра Drools, компилятора Drools и пакета интеграции Drools и Spring.

Листинг 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>
    <!-- Зависимость pom ошибочно включает зависимости spring и hibernate. -->	
  </exclusions>
</dependency>

Identity и equality

В листинге 5 я настроил глобальный сеанс анализа с сохранением состояния как singleton-компонент Spring. (Сеанс анализа без сохранения состояния не будет работать в качестве долгосрочного сеанса, потому что он не сохраняет свое состояние при итеративных вызовах.) В листинге 5 обратите внимание на <drools:assert-behavior mode="EQUALITY" />.

В JPA/Hibernate управляемые сущности сравниваются с помощью identity, а отсоединенные сущности ― с помощью equality. Сущности, введенные в сеанс анализа с сохранением состояния, быстро становятся отсоединенными с точки зрения JPA, поскольку контекст персистентности в области действия транзакции, даже «расширенный» или с «областью действия потока» (см. раздел Ресурсы), является эфемерным по сравнению с продолжительностью singleton-сеанса анализа с сохранением состояния. Одна и та же сущность, полученная через объекты с разным контекстом персистентности, каждый раз представляет собой новый Java-объект. По умолчанию Drools использует сравнение по identity. Соответственно, при поиске FactHandle для существующего факта сущности в рабочей памяти посредством ksession.getFactHandle(entity) 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" />

Подробнее детали конфигурации можно изучить по исходному коду приложения.

Правила Drools

В листинге 6 определены два правила обработки сложных событий (Complex Event Processing - CEP). Помимо двух типов фактов в виде JPA-сущностей, MemberCase и Clinician, в качестве событий объявлен класс сущностей CaseSupervision. Для каждой задачи оценки случая заболевания врачом создается запись CaseSupervision. После создания в эту запись вряд ли будут вноситься текущие изменения.

Условие правила Case Supervision в листинге 6 проверяет, производился ли осмотр пациента в течение последних 30 дней. Если нет, то часть правила consequence/action создает факт TransientReminder (определяется в листинге 7) и логически вносит этот факт в рабочую память. Правило Clinician Supervision диктует, что врач должен провести как минимум один осмотр за последние семь дней; в противном случае часть consequence/action правила создает аналогичный факт TransientReminder, который (аналогично) логически вносится в рабочую память.

Листинг 6. Правила осмотра пациентов
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  // вариант
  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

Обратите внимание, что факт TransientReminder, приведенный в листинге 7, ― не JPA-сущность, а обычный 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. Наиболее существенным различием между фактами и событиями является то, что события неизменяемы в контексте Drools. Если событие подлежит изменениям, то эти изменения («обогащение события данными») не должны влиять на результаты выполнения правила. Вот почему в EntityListener из CaseSupervision мы следим только за этапами жизненного цикла сущности @PostPersist (см. Листинг 2).

Поддержка в Drools протокола Sliding Windows делает события особенно мощным инструментом временных суждений. Скользящее окно (sliding window) ― это способ охвата интересующего нас события, как если бы оно находилось в постоянно смещающемся окне. Две наиболее распространенных реализации скользящего окна ― это временное окно и пространственное окно.

В примере правил, приведенном в листинге 6, выражение over window:time(30d) свидетельствует о том, что механизм правил оценивает события CaseSupervision, созданные в течение последних 30 дней. По истечении 30 дней неизменные события больше не попадают в окно, Drools автоматически выбирает события из рабочей памяти, и правила соответствующим образом переоцениваются. Поскольку события неизменны, Drools автоматически управляет жизненным циклом событий. Таким образом, события эффективнее используют память, чем факты. (Заметьте, однако, что в конфигурации Drools-Spring нужно задать режим обработки событий STREAM; в противном случае временные операторы, такие как скользящие окна, не будут работать.)

Работа с объявленными типами

Еще в листинге 6 примечательно то, что факт MemberCase (который не является событийным) сравнивается также с ограничением по времени, так как мы оцениваем только случаи, созданные более чем 30 дней назад. Сегодня случаю может быть 29 дней, но завтра ему будет 30 дней, что подразумевает, что правило Case Supervision должно переоцениваться в начале каждого дня. К сожалению, в Drools нет скользящей переменной «сегодня». Поэтому я добавил тип событий Today; это объявленный тип Drools, то есть конструкция данных, объявленная на языке правил, а не в коде Java.

Этот специальный тип событий не декларирует никаких явных атрибутов, а только неявный элемент метаданных @timestamp, который заполняется автоматически в тот момент, когда в рабочей памяти создается событие Today. Другой элемент метаданных, @expires(24h), указывает, что событие Today заканчивается через 24 часа после создания.

Чтобы сбрасывать Today в начале каждого дня, я также добавил к правилу Set Today таймер. Это правило активируется и срабатывает в начале каждого дня, чтобы вставить новое событие Today взамен только что истекшего. Новое событие Today вызывает переоценку правила Case Supervision. Отметим также, что таймер сам по себе не может вызвать переоценку правила без изменения факта в условиях правила. Таймер не переоценивает функции или встроенные элементы eval, потому что Drools воспринимает эти конструкции как постоянные во времени и сохраняет их значения.

Когда использовать факты, а когда ― события

Понимание разницы между фактами и событиями помогает решить, когда следует использовать тот или иной тип:

  • используйте события в сценариях, когда данные представляют собой неизменяемый снимок состояния системы в некоторый момент или на интервале времени, когда они зависят от времени и быстро истекают или когда количество данных должно быстро и непрерывно расти;
  • используйте факты в сценариях, когда данные больше относятся к предметной области и когда они испытывают постоянные изменения, которые требуют постоянной переоценки правил.

Запросы Drools

Наш следующий шаг ― извлечение результатов применения правил, для чего нужно извлечь факты из рабочей памяти. (Альтернативный подход ― передача результатов механизмом правил приложению путем вызова методов в сеансах global из правой части синтаксиса правил). В этом примере установка фактов и обработка правила происходит сразу, без каких-либо задержек, чтобы наши запросы в листинге помещались в отчеты в режиме реального времени. Так как факты TransientReminder устанавливаются логически, механизм правил автоматически удаляет их из рабочей памяти, когда условия больше не выполняются.

Допустим, что сегодня утром механизм правил создал напоминание по некоторому пациенту. Впоследствии мы выполняем запрос CaseReminderQuery в Java-коде, как показано в листинге 3, чтобы напоминание отобразилось для всех врачей в системе. Если днем врач осмотрел этого пациента и создал новую запись в истории болезни, то это событие нарушит условия факта напоминание. Тогда Drools автоматически удалит его. Чтобы убедиться, что факт напоминания исчез, можно запустить тот же запрос сразу же после завершения осмотра. Логическая установка поддерживает результаты суждений актуальными, и механизм правил работает в режиме эффективного использования памяти, подобно событиям.

Счетчик логических фактов

Обратите внимание, что каждый логически установленный факт сопровождается счетчиком, который увеличивается каждый раз при установке факта equal. Если одно из правил, которые многократно устанавливают факт equal, перестает соблюдаться, значение счетчика логических фактов уменьшается. Когда оно достигнет нуля, факт автоматически отзывается.

И напоследок ― о динамических запросах. Динамический запрос остается открытым, создавая представление результатов запросов и отображая изменения содержания данного представления. Это означает, что динамический запрос нужно выполнить только один раз, а результирующее представление будет автоматически обновляться, когда механизм правил публикует изменения.

До сих пор мы видели, что немного зная Drools, JPA и Spring, нетрудно создать приложение для непрерывного профилирования данных в режиме реального времени. В заключении мы приведем некоторые более сложные шаги, которые повысят надежность нашего решения для управления медицинской практикой.

Углубленное изучение программирования Drools

Управление отношениями

Интересное ограничение метода FactHandle заключается в том, что он связан только с текущим фактом, но не с отношениями, содержащимися в факте. Drools узнает об изменениях, произошедших в idMemberCase (хотя таких не бывает, потому что первичный ключ неизменяем), startDtm или endDtm, через свой метод FactHandle класса getKsession().update (factHandle, memberCase). Однако он не узнает об изменениях свойств member и caseSupervisions при вызове того же метода.

Аналогично, методы JPA EntityListener не оповещают об изменениях в отношениях "один ко многим" и "многие ко многим". Это объясняется тем, что внешний ключ находится в связанной (related или link) таблице.

Чтобы подключиться к этим отношениям как обновляемым фактам, можно построить рекурсивную логику для получения FactHandle каждого вложенного отношения. Лучше всего разместить перехватчики EntityListener для всех сущностей, включая таблицы ссылок, которые участвуют в формировании условий правила. Мы сделали это с помощью методов Member и CaseSupervision, где изменения обрабатываются собственными EntityListener и FactHandle каждого объекта (см. Листинг 2 и Листинг 3).

Ленивая загрузка объекта во время оценки правила

Пока мы не указали раздел базы знаний (то есть параллельную обработку), правила оцениваются в том же потоке, где вызывается ksession.insert(), ksession.update() или ksession.retract(). Установка фактов в листингах 2 и 3 происходит в контексте транзакции, где доступен контекст персистентности JPA в области транзакции (сеанс Hibernate). Это позволяет механизму правил оценивать отношение объектов с ленивой загрузкой (lazy-loaded). Если раздел базы знаний включен, нужно настроить отношения объектов как объектов с интенсивной загрузкой (eager loaded), чтобы предотвратить JPA LazyInitializationException.

Разрешение транзакций

По умолчанию Drools не поддерживает транзакции, потому что не хранит никаких исторических копий текущего состояния данных в рабочей памяти. Это проблема для наших перехватчиков EntityListener, потому что методы обратного вызова жизненного цикла вызываются после пополнения базы данных, но до совершения транзакции. А что, если произойдет откат транзакции? В этом случае объекты в контексте сохранения JPA становятся отделенными и несогласованными со строками таблиц базы данных, как и факты в рабочей памяти. Результаты суждений механизма правил становятся ненадежными.

Разрешение транзакций сделает нашу систему управления врачебной практикой надежной, гарантируя, постоянную синхронизацию данных в рабочей памяти с базой данных приложения и точность результатов суждений механизма правил. В Drools с реализациями JPA и JTA и с прописанным в classpath пакетом drools-jpa-persistenceJPAKnowledgeService (см. раздел Ресурсы) можно настроить для создания сеанса с накоплением знаний. Весь сеанс с накоплением знаний вместе с экземплярами процессов, переменными и объектами фактов отражается как двоичный столбец строки в таблице SessionInfo со значением ksessionId в качестве первичного ключа.

Когда в коде приложения мы указываем границы транзакций посредством аннотаций или XML, инициированная приложением транзакция распространяется на механизм правил. Всякий раз, когда происходит откат транзакции, сеанс с накоплением знаний будет восстанавливать предыдущее состояние, сохраненное в базе данных. Это гарантирует согласованность и интеграцию между базой данных приложения и базой данных Drools. Отдельный сеанс с накоплением знаний в памяти должен вести себя как REPEATABLE READ при одновременном доступе из нескольких транзакций JTA; в противном случае один экземпляр сущности SessionInfo может смешать изменения состояния из различных транзакций, нарушив разграничение транзакций. Отметим, что на момент написания этой статьи точно не известно, реализован ли метод REPEATABLE READ в диспетчере транзакций пакета drools-jpa-persistence.

Кластеризация

Если наше приложение будет выполняться в кластерной среде, то описанный выше подход работать не будет. Каждый экземпляр внедренного механизма правил будет получать события сущностей, происходящие только на одном узле, так что рабочая память разных узлов рассинхронизируется. Эту проблему можно решить с помощью универсального удаленного сервера Drools (см. раздел Ресурсы). Перехватчики сущностей на разных узлах публикуют все свои события на централизованном сервере Drools через каналы Web-сервисов REST/SOAP, а приложение может подписаться на результаты суждений сервера Drools. Отметим, что реализация SOAP из Apache CXF на сервере Drools в настоящее время не поддерживает ws-transaction. Я надеюсь, что такая поддержка вскоре появится, учитывая требования по поддержке транзакций, предъявляемые в рассмотренном случае практического применения.

Заключение

Эта статья помогает читателю собрать воедино уже имеющиеся у него знания в области POJO-программировании в Spring и JPA с добавлением некоторых новых возможностей Drools 5. Мы продемонстрировали, как рационально использовать EntityListener, глобальный сеанс Drools и метод fireUtilHalt() для разработки приложения непрерывного профилирования данных в режиме реального времени на основе POJO. Читатель познакомился с основными понятиями Drools, такими как работа с фактами и событиями и написание логических суждений, а также с более сложными предметами и способами их применения для управления транзакциями и распространения реализации Drools на кластерную среду. Чтобы лучше узнать Drools 5, изучите исходный код приложения.


Загрузка

ОписаниеИмяРазмер
Пример кода для статьиj-drools5-src.zip5 КБ

Ресурсы

Научиться

  • Оригинал статьи: Use Drools and JPA for continuous, real-time data profiling.
  • Главная страница проекта Drools: здесь можно узнать о новостях проекта.
  • Документация Drools: онлайн- и PDF-документы JBoss Community.
  • Implement business logic with the Drools rules engine (Ricardo Olivieri, IBM developerWorks, март 2008 г.): для начинающих пользователей Drools ― о том, как сделать Java-приложения с использованием механизма правил Drools с открытым исходным кодом более терпимыми к изменениям.
  • Introduction to Spring 2 and JPA (Sing Li, IBM developerWorks, август 2006 г.): интеграция Spring 2 с JPA ― краеугольный камень спецификации EJB 3.0. Эта статья служит практическим введением в Spring и JPA для начинающих.
  • Salaboy.com онлайн-презентации и руководства по функциям Drools.
  • WebSphere Process Server: IBM's new foundation for SOA (Wolfgang Kulhanek and Carol Serna, IBM developerWorks, сентябрь 2005 г.): о WebSphere Process Server и его возможностях, функциях и о решениях на его основе.
  • JPA with Rational Application Developer 8 and WebSphere Application Server 8 (Ali Manji, IBM developerWorks, июнь 2011 г.): инструменты Rational Application Developer для быстрого создания и настройки сущностей JPA из базы данных Derby с таблицами, использующими механизм Identity Value Generation.
  • JPA and Rollbacks — Not Pretty (David Van Couvering, Java.net, апрель 2007 г.): об откатах транзакций в JPA, которые обсуждаются в этой статье.
  • Introduction to Hibernate Search (Xinyu Liu, JavaWorld, июль 2008 г.): о механизме Hibernate Search и интеграции в него модели программирования POJO.
  • Drools JBoss Rules 5.0 Developer's Guide (Michal Bali, PACKT Publishing, июль 2009 г.): о возможностях Drools как платформы для управления бизнес-правилами.
  • Drools Developer’s Cookbook (Lucas Amador, PACKT Publishing, январь 2012 г.): ресурс для развития навыков работы с Drools 5 Expert, Fusion, Guvnor, Planner и jBPM5.

Получить продукты и технологии

  • Загрузить Drools 5: в этой статье для реализации механизма правил и среды CEP используются Drools Expert и Drools Fusion.

Обсудить

  • Примите участие в деятельности сообщества Мой developerWorks. Общайтесь с другими пользователями developerWorks, читая блоги разработчиков, форумы, группы и вики.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=859124
ArticleTitle=Использование Drools и JPA для непрерывного профилирования данных в режиме реального времени
publish-date=02222013