Blog

Prozess- und Business-Daten Hand in Hand mit Camunda und JPA

Typischerweise läuft ein automatisierter Geschäftsprozess nicht zum Selbstzweck, sondern erwirtschaftet Daten, die länger relevant sind als die eigentliche Prozessinstanz. Beispiel Buchbestellung: Während der Laufzeit des Bestellprozesses benötigen wir sowohl Informationen aus dem Prozess (Was ist als nächstes zu tun? Wer soll dies tun?) als auch Informationen aus der Domäne (Welcher Kunde hat bestellt? Was ist die zu zahlende Summe? Ist der Artikel auf Lager?) Wir haben es also mit flüchtigen Daten zu tun, die rein prozessablaufbezogen sind und persistenten Daten, die in Drittsystemen (Warenwirtschaft, Rechnungswesen, Reporting, …) verwendet werden sollen.

Es stellt sich daher grundsätzlich die Frage, wo wir diese Daten speichern.

Wo speichern wir nun die Daten?

Wir können ohne weiteres alle Daten, flüchtig und persistent, direkt an unserer Prozessinstanz speichern. Klassische Suiten bieten hierfür XSD-Strukturen, leichtgewichtige Engines wie Camunda BPM erlauben Key-Value-Stores für Prozessvariablen und Serialisierung z.B. als JSON. Vorteil: Informationen liegen genau so vor wie zur Prozessabwicklung erforderlich und können beispielsweise zur Auswertung von Gateways oder der Zuweisung von User-Tasks verwendet werden. Darüber hinaus sind alle Daten als Prozessvariablen im Cockpit einsehbar.
Problem: Die Persistenz der Prozessengine bestimmt, wo und wie meine Business-Daten gespeichert werden und dadurch auch, ob und wie Drittsysteme auf diese Daten zugreifen können. Um dies zu umgehen, geht man gern dazu über, die Daten zusätzlich zu ihrer serialisierten Form im Prozess klassisch relational (Entity) zu speichern. Problem: nun haben wir eine Redundanz erzeugt, die nur schwer in den Griff zu bekommen ist, da Prozess und Schema abweichende Daten enthalten können.

Datenarmut funktioniert für uns

Eine im Projekteinsatz bewährte alternative Methode ist strikte Datenarmut im Prozess. Alle Businessdaten verbleiben im eigenen Schema und können auch ohne Prozesskontext verwendet werden. Die laufende Prozessinstanz erhält lediglich einen fachlichen Schlüssel, beispielsweise die Bestellnummer, und lädt sich bei Bedarf die benötigten Domänendaten nach, wodurch diese stets eindeutig vorliegen. So weit, so gut. Für eine einzelne Instanz ist dies ein akzeptables Vorgehen. Zuerst wird aus den Camunda-Tabellen die entsprechende Aufgabe geladen, anschließend aus den Businesstabellen die aktuellen Fachdaten.

Wenn wir jedoch Tasklisten für die Prozessbeteiligten bereitstellen, um Aufgaben zu priorisieren und den richtigen Mitarbeitern zuzuweisen, trägt dieses Verfahren nicht mehr. Mehrere 10.000 gleichzeitig aktive Tasks sind keine Seltenheit. Wenn nun ein Mitarbeiter für einen bestimmten Kunden zuständig ist, wir jedoch nur die Bestellnummer kennen (Datenarmut), müssten wir, um festzustellen welche Aufgaben diesem Mitarbeiter zugewiesen werden sollen, für alle aktiven Tasks die Businessdaten nachladen, was ein nicht akzeptables Laufzeitverhalten garantiert (n+1-Problem).

Custom Queries in Camunda…

Camunda liefert uns eine Lösung: Custom Queries. Und damit sind wir beim eigentlichen Grund für diesen Artikel angekommen.
Ohne zu sehr auf Details eingehen zu wollen, die dokumentierten Custom Queries machen keinen Spaß. Es handelt sich nicht um eine integrierbare Lösung, sondern um ein „Best Practice“, das relativ aufwändig ins eigene Projekt integriert werden muss (Copy-und-paste-Code, mapping.xml-Dateien, händisch überschriebene Factoryklassen) und darüber hinaus die Verwendung von Camundas internem O/R-Mapping „MyBatis“ aufdrängt, was insbesondere für Anwender von JPA(Hibernate) einen kompletten Bruch und nicht zuletzt eine erhebliche Lernkurve bedeutet.

… und unsere vereinfachte Lösung!

Doch es geht besser: Statt mühsam ein MyBatis-Mapping und Custom Queries für unsere Business Entities aufzubauen, drehen wir den Spieß einfach um: wir betrachten einfach camunda-Tasks als JPA-Entity und lassen den übrigen Stack wie er war.
Der Kern der Idee ist, dass ein Task, wenn ich ihn nur aus der Perspektive der Taskliste betrachte, nur ein nicht-modifizierbares Query-Objekt darstellt. Die Anweisung „Gib mir alle Aufgaben für Kunde X“ liefert fünf Instanzen, die ich zwar darstellen, nicht jedoch direkt verändern möchte, dafür werden Services genutzt (complete(taskId)).

Somit muss nur die existierende Camunda-Task-Tabelle als View bereitgestellt werden, anschließend können wir per Entity/Repository darauf zugreifen:

CREATE OR REPLACE VIEW TASK_WITH_ORDER AS
SELECT DISTINCT
T.ID_ AS TASK_ID,
T.REV_ AS VERSION,
T.NAME_ AS NAME,
T.DESCRIPTION_ AS DESCRIPTION,
T.TASK_DEF_KEY_ AS TASK_DEFINITION_KEY,
-- the businessKey is our orderId, we can use it to map with a domain model.
P.BUSINESS_KEY_ AS ORDER_ID,
T.PRIORITY_ AS PRIORITY,
T.CREATE_TIME_ AS CREATED_DATE,
T.DUE_DATE_ AS DUE_DATE,
T.FOLLOW_UP_DATE_ AS FOLLOW_UP_DATE,
T.PROC_INST_ID_ AS PROCESS_INSTANCE_ID,
T.PROC_DEF_ID_ AS PROCESS_DEFINITION_ID,
T.ASSIGNEE_ AS ASSIGNEE
FROM ACT_RU_TASK T
LEFT OUTER JOIN ACT_RU_EXECUTION P ON P.ID_ = T.PROC_INST_ID_;

und die dazugehörige Entity:

public class TaskWithOrder implements Serializable {

  // use the unique id from camunda task
  @Id
  private String taskId;

  // important! JPA-version based on task revision, otherwise JPA-cache is not refreshed
  @Version
  private Integer version;

  // the tasks display name
  private String name;

  // further camunda task attributes
  private String description;
  private ....

  // the "magic" part: link the camunda task from the view to our Business-Model. Note that the Process' businesskey
  // is mapped to orderId for this to happen.
  @OneToOne
  private OrderEntity order;
}

Das Schreiben spezialisierter Anfragen für die eigene TaskList-Anwendung ist nun denkbar einfach, mit Spring Data beispielsweise muss nur ein Interface definiert werden:

@Repository
public interface TaskWithOrderRepository extends org.springframework.data.repository.Repository<TaskWithOrder, String> {

  TaskWithOrder findOne(String id);
  List<TaskWithOrder> findAll();
}

Das komplette Beispiel (Spring-Boot-Anwendung mit Book Order Process) ist auf github verfügbar: camunda-example-customquery.

Über den Autor

Jan Galinski

Jan Galinski ist Senior Consultant bei der Holisticon AG und seit vielen Jahren als Architekt und Entwickler in agilen Kundenprojekten unterwegs.
Er ist ein leidenschaftlicher Prozessautomatisierer und BPM-Craftsman, am liebsten mit Camunda BPM.
Als Contributor zu zahlreichen Open Source Projekten aus den Bereichen BPM und JEE gibt er seine Erfahrung und Wissen gerne weiter. 2016 wurde er mit dem Camunda Community Award ausgezeichnet.

Antwort hinterlassen