Как стать автором
Обновить
93.32
Haulmont
Корпоративные системы и инструменты разработчика

Всё о процессных переменных, что должен знать BPM-разработчик. Часть 1

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров1.2K

Эта статья продолжает цикл BPMN: Beyond the Basics – о скрытых нюансах и подводных камнях BPMN для разработчиков. Сегодня поговорим о процессных переменных — для чего они используются в процессе, чем отличаются от переменных в языках программирования и как работают области видимости. Казалось бы, что тут обсуждать? — Однако, если погрузиться на уровень поглубже аналитического, то обнаруживается много интересного. Поэтому в одну статью даже не поместилось, пришлось делить на две части.

Вторая часть здесь.

Данные в процессе

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

Для процессов описательного уровня такой подход вполне оправдан. Человек действительно обязан знать, что где лежит, и способен ориентироваться в контексте. А чтоб он не приставал с вопросами, для него делают должностную инструкцию на основе модели процесса.

Но всё меняется, когда дело доходит до автоматизированных или, тем более, полностью автоматических  процессов, где человека в принципе нет. Такой процесс — уже не просто цепочка действий, а механизм обработки данных. И, чтобы он работал не только логически правильно, а еще быстро и надежно, надо понимать, как данные в него передаются, хранятся и используются.

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

Data-centric процессы и BPM-движки

Если бы это была статья для CNews или TAdviser, то здесь полагалось бы вставить цитату от каких-нибудь авторитетов типа Gartner или Forrester, о том, как важны для бизнеса data-centric процессы. Но обойдемся без занудства! Ведь вполне очевидно, что фокус смещается в сторону автоматизированных процессов — недаром разработчики BPM все больше говорят об оркестрации микросервисов и тому подобных вещах. Конечно, остаются процессы типа документооборота, где надо согласовывать разные документы, но сейчас речь о другом — о полностью автоматических процессах, где на первый план выходит обработка данных.

Давайте же посмотрим, как к этому готовы наши BPM-движки. Спойлер: ну так себе.

Нотация BPMN изначально создавалась как язык описания процессов и не включает модели данных. Все, что в ней есть на эту тему — так называемый Data Object, который лишь намекает, что в процессе используются какие-то даже не просто данные, а документы судя по его значку. И еще есть Data Store, притворяющийся базой данных или некой информационной системой, опять-таки судя по его изображению.

По сути, это лишь графические символы. Их роль исчерпывается тем, чтобы сообщить читателю диаграммы, что в системе ходят какие-то данные и есть взаимодействие с неким хранилищем. Никакой имплементации на уровне движка для них не существует. И получается такая ситуация, что для моделирования непосредственно процесса есть четкие правила согласно стандарту BPMN 2.0, которые в разных движках реализованы если не одинаково, то очень похоже, а унифицированного механизма работы с данными нет. В этой части каждый разработчик решает сам, что ему делать. С одной стороны, это хорошо, это свобода! — можно выбрать оптимальное решение для своей задачи. С другой, отсутствие четких правил приводит к тому, что в проектах принимаются, мягко скажем, не лучшие решения по работе с данными.

Для чего нужны процессные переменные

Итак, на уровне BPMN-модели у нас нет данных как таковых — схема описывает структуру и логику процесса, но не оперирует переменными напрямую. Однако при выполнении процесса данные становятся необходимы: где-то нужно сохранить пользовательский ввод, где-то принять решение, а где-то — отправить запрос во внешнюю систему. Все эти данные существуют в виде процессных переменных, которые «прикреплены» к каждому экземпляру процесса и сопровождают его на всём пути выполнения.

Укрупненно, можно назвать четыре роли, которые выполняют процессные переменные:

  • Передача данных между шагами

  • Управление ходом процесса

  • Взаимодействие с пользователем

  • Интеграция с внешними системами

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

Они также позволяют управлять поведением процесса в ходе выполнения. Допустим, у нас есть переменная priority, которая вычисляется на основе параметров заявки. Если при старте процесса она равна "high", задача автоматически направляется конкретному специалисту, иначе — помещается в общую очередь.

Когда процесс взаимодействует с пользователем, переменные становятся связующим звеном между формой и логикой процесса. Они используются для отображения данных и  для приёма пользовательского ввода. Если переменная уже была установлена на предыдущем шаге — например, userEmail, — её значение можно показать в форме. Пользователь, в свою очередь, заполняет другие поля, и их значения сохраняются обратно в переменные, чтобы использовать их дальше по процессу. Таким образом, форма работает как «окно» в текущий контекст выполнения: всё, что процесс знает на этот момент, доступно пользователю, и всё, что он введёт — останется в процессе.

Наконец, процессные переменные — это канал связи с внешним миром. Когда сервисная задача вызывает REST API, она может использовать переменные как входные параметры и сохранять ответ в переменную. Далее этот ответ можно анализировать, логировать, передавать в другой сервис или показывать пользователю.

Жизненный цикл переменных

Теперь давайте поговорим, как процессные переменные рождаются, живут и заканчивают свой путь. Для этого сначала посмотрим, как они устроены. В BPM-системах, таких как в Camunda, Flowable или Jmix BPM , процессные переменные представляют собой объекты, хранящие не только значение, но и метаинформацию о себе. То есть, это некий контейнер, в котором лежат данные, а не обычная переменная, как в Java или другом языке. Зачем так сложно? — Да потому что процесс может выполняться долго — часы, дни или даже месяцы. И поэтому приходится сохранять значения переменных в базу, чтобы потом извлечь оттуда, когда они понадобятся. А если мы записываем что-то в БД, то появляются и метаданные, все логично.

Создание и сохранение переменных

Замечание: далее в этом разделе примеры приведены на основе движка Flowable

Итак, в процесс поступили данные. Например, как payload в стартовом сообщении в виде Map<String, Object>. Что дальше делает движок? — Прежде всего, он создает экземпляр процесса — ProcessInstance, инициализируя его контекст выполнения. Далее движок автоматически сохраняет все переданные переменные в этот контекст используя метод setVariable. Но важно понимать: движок не просто "кладёт" значения куда-то в память. Он оборачивает каждую переменную с учётом её типа во внутреннюю сущность VariableInstanceEntity и тогда эти переменные сразу становятся доступными в любом элементе процесса — скриптах, условиях перехода, задачах и т.д. Также, разработчик может создать переменную в коде, когда это нужно, тоже посредством метода setVariable, в том числе и в Groovy-скриптах:

runtimeService.setVariable(executionId, "docStatus", "APPROVED"); 

Пока процесс не достиг границы транзакции, запись в базу не производится. Переменная остается в in-memory структуре List в составе ExecutionEntity. Это удобно: следующая задача или скрипт могут использовать её без обращения к базе. А вот когда на пути процесса встречается событие ожидания сообщения или сигнала, таймер, пользовательская задача, event-based gateway или асинхронная задача, тогда-то и закрывается коммит, а все сущности, созданные или обновленные за время транзакции сливаются в базу командой flush. Включая процессные переменные, которые записываются в таблицу ACT_RU_VARIABLE.

Если тип переменной относится к примитивным, то она сохраняется как есть в соответствующее поле в таблице. А непримитивные переменные перед сохранением сериализуются и сохраняются в виде строк или массива байтов.

Поле

Тип

Назначение

id_

varchar(64)

Уникальный идентификатор переменной. Первичный ключ таблицы.

rev_

integer

Версия записи (используется для optimistic locking при обновлении переменной).

type_

varchar(255)

Тип переменной (например, string, long, boolean, serializable, json, object, bytearray, и т.п.).
Указывает, какие поля (text_, long_, bytearray_id_ и др.) содержат значение.

name_

varchar(255)

Имя переменной.

execution_id_

varchar(64)

ID контекста исполнения (execution), к которому относится переменная.

proc_inst_id_

varchar(64)

ID процесса, к которому относится переменная.

task_id_

varchar(64)

ID задачи, если переменная установлена на уровне задачи.

scope_id_

varchar(255)

Используется только в CMMN.

sub_scope_id_

varchar(255)

Используется только в CMMN.

scope_type_

varchar(255)

Тип области, к которой привязана переменная (bpmn, cmmn, dmn).
На практике не используется.

bytearray_id_

varchar(64)

ID в таблице act_ge_bytearray, где хранится значение переменной, если это массив байтов или сериализованный объект.

double_

double precision

Значение переменной, если тип — double.

long_

bigint

Значение переменной, если тип — long, integer, short, boolean (в виде 0/1).

text_

varchar(4000)

Значение переменной, если тип — string, json, date, uuid.
Также может содержать сериализованные значения (в виде текста).

text2_

varchar(4000)

Дополнительное текстовое поле, например, для хранения формата сериализации или дополнительных параметров. Может использоваться в JSON/XML сериализации.
Jmix BPM хранит здесь ссылки на переменные-сущности в формате :
<class_name>.<UUID>

meta_info_

varchar(4000)

Метаданные о переменной, например, класс объекта (если это сериализованный объект), или другие сведения, полезные движку.
B Jmix BPM не используется.

После того как переменная записана в таблицу ACT_RU_VARIABLE, она становится частью персистентного состояния процесса. Это значит, что даже если сервер будет перезапущен, процесс можно восстановить и продолжить — вместе со всеми переменными. В этот момент также сбрасывается кеш и объекты VariableInstanceEntity удаляются из памяти.

В некоторых случаях наоборот требуется, чтобы переменная не хранилась в БД. Например, результаты промежуточных вычислений, которые не нужны на дальнейших шагах процесса. Или чувствительные данные — пароли, токены авторизации и тому подобное. Тогда переменные объявляются транзиентными и хранятся только в памяти. Но это на усмотрение разработчика, по умолчанию все переменные персистентны.

Чтение и обновление переменных

Рассмотрим теперь, как происходит чтение переменных.

После успешного завершения одной транзакции начинается выполнение новой и кеш переменных создается заново. Сначала он пуст, переменные не грузятся по умолчанию. Только когда переменная действительно для чего-то нужна, выполняется команда типа

String status = (String)runtimeService.getVariable(executionId, "docStatus");

Сначала движок по заданному executionId находит соответствующий объект ExecutionEntity. Это "контекст выполнения", в котором хранятся переменные, связанные с конкретным активным шагом процесса. Если переменная еще не находится в памяти, движок делает SQL-запрос к таблице ACT_RU_VARIABLE. Полученный объект десериализуется (если нужно), добавляется в кэш ExecutionEntity и затем возвращается вызывающему коду. В случае, когда вам нужно не просто значение переменной, а полная информация, включая метаданные, можно запросить объект VariableInstance:

VariableInstance statusVar = runtimeService.getVariableInstanse(executionId, "docStatus");

Но учтите, это будет read-only объект. Если нужно обновить значение переменной, то снова выполняется команда setVariable, и при следующем коммите новое значение запишется в базу. Строго говоря, это не обновление, а создание новой переменной с таким же именем. И тут есть тонкий момент: движок не проверяет соответствие типов. То есть, если у вас в переменной лежала строка, а потом в нее сохранили число, движок это спокойно примет. Но в дальнейшем могут быть проблемы при доступе к историческим данным или при использовании переменной на других шагах процесса.

Удаление переменных

Когда процесс (или выполнение, execution) завершается, его переменные удаляются. То есть, из таблицы ACT_RU_VARIABLE удаляются все записи, относящиеся к конкретному executionId. Также, разработчик может удалить переменную в инициативном порядке, не дожидаясь завершения процесса:

runtimeService.removeVariable(executionId, "largePayload");

Следует сказать, что в нормальном режиме, просто чтобы «навести порядок», этого делать не требуется, движок сам разберется.

Однако, в некоторых ситуациях превентивное удаление переменных может быть полезно. Например, когда в переменной содержится большой объем данных, допустим, JSON размером в десяток мегабайт или картинка в высоком разрешении. Но не забывайте, что эти данные хранятся в базе данных, а не в памяти Java-процесса, поэтому речь идёт не о сборке мусора, а о разгрузке хранилища.

Если переменная содержит персональные или чувствительные данные (например, временные пароли, одноразовые коды) и они не нужны после использования, их следует удалить.

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

А чтобы совсем не заморачиваться с удалением переменных, лучше сразу объявить их транзиентными.

Запись в историю

Ключевой принцип в том, что исторические записи переменных создаются не по завершении процесса, а по факту их установки (setVariable), если ведение истории вообще включено. Это определяется параметром historyLevel.

historyLevel

Описание

Что сохраняется

none

История полностью отключена

Ничего не сохраняется

activity

Минимальная история: отслеживаются активности процессов

Запуск/завершение процессов, задачи, события

audit

Более подробная история

Всё из уровня activity плюс переменные (только последнее значение)

full

Полная история, включая изменения

Всё из уровня audit плюс все изменения переменных

По умолчанию установлен режим audit. Это значение выбрано как компромисс между полезностью истории и производительностью. То есть, обычно история включена. В этом случае, когда переменная создается, ее значение параллельно еще записывается в таблицу ACT_HI_VARINST. Точнее, создается сущность HistoricVariableInstanceEntity, которая заносится в эту таблицу, когда закрывается транзакция. А когда historyLevel=full, то в историю записывается каждое изменение переменной, в таблицу ACT_HI_DETAIL.

Важное замечание

BPM-движки не предоставляют единого механизма по работе с переменными. Они могут появиться в любом месте — как поле пользовательской формы, в виде полезной нагрузки в сообщении или сигнале, быть объявлены в коде скрипта, Java-делегата или Spring-бина.

И вся эта кухня целиком на ответственности разработчика. IDE тут ничем помочь не может. Поэтому требуется крайняя внимательность. Вам однажды захочется поправить опечатку в названии переменной — но нельзя будет автоматически отследить все места, где она используется.

Подписывайтесь на наш телеграм-канал
BPM Developers — про бизнес-процессы: новости, гайды, полезная информация и юмор.

Во второй части мы разберем, как работают области видимости, какие бывают типы переменных, существующие ограничения и особенности работы с переменными в Groovy-скриптах.

Теги:
Хабы:
+5
Комментарии0

Публикации

Информация

Сайт
www.haulmont.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия
Представитель
Haulmont