Статьи

Настойчивость в Активити 6

В последние годы мы часто слышали просьбу (как от сообщества, так и от наших клиентов) о том, как поменять логику постоянства Activiti с реляционной базы данных на что-то другое. Когда мы анонсировали Activiti 6, одним из обещаний было то, что мы сделаем именно это возможным.

Люди, которые погрузились в код движка Activiti, будут знать, что это серьезный рефакторинг, поскольку постоянный код тесно связан с обычной логикой. В основном в Activiti v5 были:

  • Классы сущностей : они содержат данные из базы данных. Обычно одна строка базы данных является одним экземпляром сущности
  • EntityManager : эти классы группируют операции, связанные с сущностями (поиск, удаление,… методы)
  • DbSqlSession : низкоуровневые операции (CRUD) с использованием MyBatis . Также содержит кэши длительности команд и управляет сбросом данных в базу данных.

Проблемы в версии 5 были следующие:

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

Не поймите меня неправильно. Код v5 продвинул нас очень далеко и привел в действие множество удивительных вещей по всему миру. Но когда дело доходит до замены слоя постоянства … это не то, чем можно гордиться.

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

Нет, для версии 6 мы хотели сделать это правильно . И, о боже … мы знали, что это будет много работы … но это было еще больше работы, чем мы могли себе представить (просто посмотрите на коммиты в ветке v6 за последние пару недель). Но мы сделали это … и конечный результат просто прекрасен (я предвзят, правда). Итак, давайте посмотрим на новую архитектуру в v6 (простите мне мои картинки Powerpoint. Я кодер, а не дизайнер!):

Screenshot-2015-09-17-17.14.00

Итак, там, где в v5 не было интерфейсов, везде есть интерфейсы в v6. Приведенная выше структура применяется для всех типов сущностей в движке (в настоящее время около 25). Так, например, для TaskEntity есть TaskEntityImpl , TaskEntityManager , TaskEntityManagerImpl , TaskDataManager и класс TaskDataManagerImpl (и да, я знаю, что они все еще нуждаются в javadoc). То же самое относится ко всем лицам .

Позвольте мне объяснить диаграмму выше:

  • EntityManager : это интерфейс, с которым весь остальной код взаимодействует, когда дело касается данных. Это единственная точка входа, когда речь идет о данных для конкретного типа объекта.
  • EntityManagerImpl : реализация класса EntityManager. Операции часто выполняются на высоком уровне и выполняют несколько задач одновременно. Например, удаление Execution может также удалять задачи, задания, identityLinks и т. Д. И запускать соответствующие события. Каждая реализация EntityManager имеет DataManager. Всякий раз, когда ему нужны данные из постоянного хранилища, он использует этот экземпляр DataManager для получения или записи соответствующих данных.
  • DataManager: этот интерфейс содержит операции «низкого уровня». Обычно содержит методы CRUD для типа сущности, которым он управляет, и конкретные методы поиска, когда необходимы данные для конкретного варианта использования
  • DataManagerImpl : реализация интерфейса DataManager. Содержит фактический код персистентности. В v6 это единственный класс, который теперь использует классы DbSqlSession для связи с базой данных с использованием MyBatis. Обычно это тот класс, который вы хотите поменять.
  • Entity : интерфейс для данных. Содержит только геттеры и сеттеры.
  • EntityImpl : реализация вышеуказанного интерфейса. В Activiti v6 это обычное pojo, но интерфейс позволяет переключаться на различные технологии, такие как Neo4 с spring-dataj, JPA,… (которые используют аннотации). Без этого вам нужно будет обернуть / развернуть объекты, если реализация по умолчанию не будет работать на вашей технологии постоянства.

Укрепление

Перемещение всех операций в интерфейсы дало нам четкое представление о том, какие методы были распределены по базе кода. Знаете ли вы, например, что существует по крайней мере пять различных способов удаления Выполнения (названных «удалить», «удалить», «уничтожить» и т. Д.) Они сделали почти то же самое, но с небольшими различиями. Или иногда не совсем тонкий.

Большая часть работы за последние недели включала объединение всей этой логики в один метод. Теперь, в текущей кодовой базе, есть только один способ сделать что-то. Что очень важно для людей, которые хотят использовать разные технологии персистентности. Заставить их реализовать все разновидности и тонкости было бы безумием.

Реализация в памяти

Чтобы доказать возможность подключения слоя постоянства, я сделал небольшой прототип «в памяти». Это означает, что вместо реляционной базы данных мы используем простые старые HashMaps для хранения наших сущностей как {entityId, entity}. Затем запросы становятся условными предложениями.

(Люди иногда на форуме спрашивали, как трудно было запускать Activiti исключительно в памяти, для простых случаев использования, которые не требуют использования базы данных. Ну, теперь это совсем не сложно! Кто знает … этот маленький прототип может стать чем-то, если людям это понравится!)

  • Как и ожидалось, мы обменяем реализации DataManager с нашей версией в памяти, см. InMemoryProcessEngineConfiguration
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Override
 protected void initDataManagers() {
  
   this.deploymentDataManager = new InMemoryDeploymentDataManager(this);
   this.resourceDataManager = new InMemoryResourceDataManager(this);
   this.processDefinitionDataManager = new InMemoryProcessDefinitionDataManager(this);
   this.jobDataManager = new InMemoryJobDataManager(this);
   this.executionDataManager = new InMemoryExecutionDataManager(this);
   this.historicProcessInstanceDataManager = new InMemoryHistoricProcessInstanceDataManager(this);
   this.historicActivityInstanceDataManager = new InMemoryHistoricActivityInstanceDataManager(this);
   this.taskDataManager = new InMemoryTaskDataManager(this);
   this.historicTaskInstanceDataManager = new InMemoryHistoricTaskInstanceDataManager(this);
   this.identityLinkDataManager = new InMemoryIdentityLinkDataManager(this);
   this.variableInstanceDataManager = new InMemoryVariableInstanceDataManager(this);
   this.eventSubscriptionDataManager = new InMemoryEventSubscriptionDataManager(this);
  
 }

Такие реализации DataManager довольно просты. Посмотрите, например, InMemoryTaskDataManager, которому необходимо реализовать методы извлечения / записи данных для TaskEntity:

1
2
3
4
5
6
7
8
9
public List<TaskEntity> findTasksByExecutionId(String executionId) {
   List<TaskEntity> results = new ArrayList<TaskEntity>();
   for (TaskEntity taskEntity : entities.values()) {
     if (taskEntity.getExecutionId() != null && taskEntity.getExecutionId().equals(executionId)) {
       results.add(taskEntity);
     }
   }
 return results;
 }

Чтобы доказать, что это работает, давайте развернем, запустим простой экземпляр процесса, выполним небольшой запрос задачи и проверим историю Этот код в точности совпадает с «обычным» использованием Activiti.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
  
 public static void main(String[] args) {
   InMemoryProcessEngineConfiguration config = new InMemoryProcessEngineConfiguration();
   ProcessEngine processEngine = config.buildProcessEngine();
  
   RepositoryService repositoryService = processEngine.getRepositoryService();
   RuntimeService runtimeService = processEngine.getRuntimeService();
   TaskService taskService = processEngine.getTaskService();
   HistoryService historyService = processEngine.getHistoryService();
  
   Deployment deployment = repositoryService.createDeployment().addClasspathResource("oneTaskProcess.bpmn20.xml").deploy();
   System.out.println("Process deployed! Deployment id is " + deployment.getId());
  
   ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess");
   List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
   System.out.println("Got " + tasks.size() + " tasks!");
  
   taskService.complete(tasks.get(0).getId());
   System.out.println("Number of process instances = " + historyService.createHistoricProcessInstanceQuery().count());
   System.out.println("Number of active process instances = " + historyService.createHistoricProcessInstanceQuery().finished().count());
   System.out.println("Number of finished process instances = " + historyService.createHistoricProcessInstanceQuery().unfinished().count());
  
 }
 
}

Что, если вы запустите, это даст вам это (невероятно быстро, поскольку все это в памяти!):

1
2
3
4
5
6
7
8
9
Process deployed! Deployment id is 27073df8-5d54-11e5-973b-a8206642f7c5
 
Got 1 tasks!
 
Number of process instances = 1
 
Number of active process instances = 0
 
Number of finished process instances = 1

В этом прототипе я не добавил транзакционную семантику. Это означает, что если два пользователя одновременно выполнят одну и ту же задачу, результат будет неопределенным. Конечно, возможно иметь логику транзакций в памяти, которую вы ожидаете от Activiti API, но я еще не реализовал это. По сути, вам нужно будет хранить все объекты, к которым вы прикасаетесь, в небольшом кэше до времени сброса / фиксации и выполнять некоторую блокировку / синхронизацию в этой точке. И, конечно же, я принимаю запросы на тягу 🙂

Что дальше?

Ну, это в значительной степени зависит от вас. Дайте нам знать, что вы думаете об этом, попробуйте!

Мы находимся в тесном контакте с одним из членов / клиентов нашего сообщества, которые планируют опробовать его в ближайшее время. Но мы, конечно же, тоже хотим поиграть с этим самим, и мы смотрим на то, что было бы круто для первого выбора (у меня все еще есть особое место в моем сердце для Neo4j … что было бы прекрасно, так как это транзакционно).

Но самое важное: в Activiti v6 теперь можно чисто поменять постоянный слой. Мы очень гордимся тем, как это выглядит сейчас. И мы надеемся, что вам тоже понравится!