Chào bạn,
Trong bài học trước, chúng ta đã tìm hiểu các cách để "bơm" dữ liệu vào một quy trình nghiệp vụ thông qua việc khởi tạo biến quy trình (process variables). Bạn đã biết cách tạo biến khi bắt đầu một quy trình bằng Camunda Modeler, REST API và code Java.
Hôm nay, chúng ta sẽ đi sâu hơn vào vòng đời của dữ liệu. Mục tiêu của bài học này là để bạn hiểu rõ cách truyền biến giữa các tác vụ (tasks) trong một quy trình. Đây là cơ chế cốt lõi cho phép các bước khác nhau trong quy trình giao tiếp và chia sẻ thông tin với nhau, làm cho quy trình trở nên liền mạch và thông minh.
1. Cơ chế hoạt động: Phạm vi (Scope) của Biến
Câu hỏi quan trọng nhất là: "Làm thế nào mà một biến được tạo ở Task A lại có thể được truy cập ở Task B?"
Câu trả lời nằm ở khái niệm phạm vi của biến (variable scope). Trong Camunda, theo mặc định, một biến quy trình có phạm vi toàn cục trong một thực thể quy trình (process instance).
Hãy tưởng tượng mỗi process instance như một đối tượng lớn. Khi bạn tạo một biến bằng execution.setVariable("myVar", "myValue"), bạn đang thực chất thêm một thuộc tính vào đối tượng đó. Do đó, bất kỳ tác vụ (task) nào khác bên trong cùng một process instance đều có thể đọc, và thậm chí sửa đổi, thuộc tính đó.
Để hiểu sâu hơn về vòng đời và phạm vi của biến, bạn hãy đọc bài viết sau. Đây là một tài liệu rất hay giải thích các khái niệm nền tảng.
Everything a BPM Developer Should Know About Process Variables
Bài viết 'Everything a BPM Developer Should Know About Process Variables' cung cấp một cái nhìn tổng quan tuyệt vời về cách các biến hoạt động trong một BPM engine. Nó sẽ giúp bạn củng cố kiến thức nền tảng.
Hãy tập trung đọc các phần sau: Bắt đầu từ đầu đến hết phần 'Why Do We Need Process Variables?': Để hiểu vai trò của biến, đặc biệt là vai trò 'Data transfer between steps' (truyền dữ liệu giữa các bước). Phần 'The Lifecycle of a Variable': Chú ý đến cách biến được tạo, lưu trữ và cập nhật. Phần 'Scope': Đây là phần quan trọng nhất cho bài học hôm nay. Nó giải thích cách các biến được chia sẻ trong một process instance thông qua hệ thống phân cấp execution context.
Như bài viết đã giải thích, do các biến được gắn vào execution context và các context con có thể truy cập biến của context cha, một biến được tạo ở cấp độ quy trình sẽ tự động hiển thị cho tất cả các tác vụ bên trong nó.

2. Truyền và Đọc biến trong các loại Task khác nhau
Mặc dù cơ chế scope là chung, cách bạn tương tác với biến (đọc/ghi) sẽ hơi khác nhau tùy thuộc vào loại task bạn đang sử dụng.
2.1. Script Task
Script Task, thường dùng Javascript, là cách nhanh chóng để thao tác với biến. Bạn có thể truy cập trực tiếp các biến bằng tên của chúng hoặc thông qua đối tượng execution.
Bài viết sau đây có một ví dụ rất trực quan.
Camunda Script and Service Task Implementation | by Praveenbuya
Bài viết 'Camunda Script and Service Task Implementation' của Praveenbuya cho thấy một ví dụ đơn giản về việc truyền biến giữa các Script Task.
Hãy đọc hai phần đầu tiên, 'Script Task: Inline Script to print JSON object' và 'Script Task: External JavaScript Resource'. Trong phần đầu, một Script Task tạo ra một biến tên là UserObject. Ngay sau đó, trong phần hai, một Script Task khác đọc chính biến UserObject đó để in ra console. Đây là minh chứng rõ ràng nhất cho việc truyền biến tự động.
Trong Script Task, bạn có thể:
- Đọc biến:
var myValue = execution.getVariable('myVariable');hoặc đơn giản làvar myValue = myVariable; - Ghi/Cập nhật biến:
execution.setVariable('myVariable', 'newValue');

2.2. Service Task (JavaDelegate)
Đối với bạn là một developer, đây là cách tương tác phổ biến nhất. Như đã học ở bài trước, bạn sử dụng đối tượng DelegateExecution được truyền vào phương thức execute.
- Đọc biến:
String orderId = (String) execution.getVariable("orderId"); - Ghi/Cập nhật biến:
execution.setVariable("processingStatus", "Completed");
2.3. User Task
Với User Task, biến thường được dùng cho hai mục đích:
- Hiển thị dữ liệu: Các biến hiện có sẽ được dùng để điền sẵn thông tin vào các trường trong form (ví dụ: hiển thị thông tin đơn hàng cần duyệt).
- Thu thập dữ liệu: Dữ liệu người dùng nhập vào form sẽ được lưu lại thành các biến mới hoặc cập nhật các biến đã có.
Khi bạn hoàn thành một task trong Camunda Tasklist và thêm các biến, những biến đó sẽ được thêm vào scope của process instance và có sẵn cho các task tiếp theo. Chúng ta sẽ tìm hiểu kỹ hơn về việc xây dựng form trong Module 10.
3. Hoạt động thực hành: Xây dựng luồng dữ liệu
Bây giờ, hãy cùng áp dụng những kiến thức trên vào một bài thực hành nhỏ. Chúng ta sẽ tạo một quy trình nơi dữ liệu được khởi tạo ở một bước, được người dùng bổ sung ở bước tiếp theo, và cuối cùng được xử lý bởi một Service Task.
Quy trình: Start Event -> Service Task "Khởi tạo Đơn hàng" -> User Task "Duyệt Đơn hàng" -> Service Task "Xử lý Kết quả" -> End Event.
Bước 1: Cấu hình Service Task "Khởi tạo Đơn hàng"
-
Trong project Spring Boot của bạn, tạo một lớp Java
InitializeOrderDelegateimplementJavaDelegate. -
Trong phương thức
execute, tạo một vài biến ban đầu cho đơn hàng.package com.example.camunda; import org.camunda.bpm.engine.delegate.DelegateExecution; import org.camunda.bpm.engine.delegate.JavaDelegate; import org.springframework.stereotype.Component; import java.util.UUID; @Component("initializeOrder") public class InitializeOrderDelegate implements JavaDelegate { @Override public void execute(DelegateExecution execution) throws Exception { String orderId = UUID.randomUUID().toString(); double amount = Math.round(Math.random() * 1000 * 100.0) / 100.0; // Số tiền ngẫu nhiên execution.setVariable("orderId", orderId); execution.setVariable("orderAmount", amount); System.out.println("Đã khởi tạo đơn hàng: ID=" + orderId + ", Số tiền=" + amount); } } -
Trong Camunda Modeler, tạo Service Task đầu tiên và cấu hình Implementation của nó là
Delegate Expressionvới giá trị là${initializeOrder}.
Bước 2: Cấu hình User Task "Duyệt Đơn hàng"
- Tạo một User Task ngay sau Service Task trên.
- Đặt tên cho nó, ví dụ: "Duyệt đơn hàng".
- (Không cần cấu hình gì thêm ở bước này).
Bước 3: Cấu hình Service Task "Xử lý Kết quả"
-
Tạo một lớp Java
ProcessResultDelegateimplementJavaDelegate. -
Trong phương thức
execute, lớp này sẽ đọc các biến đã có (orderId,orderAmount) và cả biến mà người dùng sẽ thêm vào ở User Task (approved).package com.example.camunda; import org.camunda.bpm.engine.delegate.DelegateExecution; import org.camunda.bpm.engine.delegate.JavaDelegate; import org.springframework.stereotype.Component; @Component("processResult") public class ProcessResultDelegate implements JavaDelegate { @Override public void execute(DelegateExecution execution) throws Exception { // Đọc các biến từ các bước trước String orderId = (String) execution.getVariable("orderId"); Double amount = (Double) execution.getVariable("orderAmount"); Boolean approved = (Boolean) execution.getVariable("approved"); System.out.println("--- Bắt đầu xử lý kết quả ---"); System.out.println("Đơn hàng ID: " + orderId); System.out.println("Số tiền: " + amount); if (approved != null && approved) { System.out.println("Kết quả: Đơn hàng đã được DUYỆT."); // Thêm logic xử lý khi được duyệt... } else { System.out.println("Kết quả: Đơn hàng đã bị TỪ CHỐI hoặc chưa có quyết định."); // Thêm logic xử lý khi bị từ chối... } System.out.println("----------------------------"); } } -
Trong Modeler, tạo Service Task cuối cùng và cấu hình Implementation của nó là
Delegate Expressionvới giá trị là${processResult}.
Bước 4: Chạy và kiểm tra
- Deploy quy trình của bạn.
- Vào Camunda Tasklist và bắt đầu một process instance mới.
- Bạn sẽ thấy một task "Duyệt đơn hàng" xuất hiện.
- Claim task đó. Ở form bên phải, nhấn vào tab Variables. Bạn sẽ thấy
orderIdvàorderAmountđã được tạo bởi Service Task đầu tiên. - Bây giờ, hãy nhấn vào nút Add Variable.
- Name:
approved - Type:
Boolean - Value:
true(check vào ô checkbox)
- Name:
- Nhấn nút Complete.
- Kiểm tra console của ứng dụng Spring Boot. Bạn sẽ thấy output từ lớp
ProcessResultDelegate, chứng tỏ nó đã đọc thành công cả các biến từ Service Task đầu tiên và biếnapprovedbạn vừa thêm ở User Task.
4. Trường hợp đặc biệt: Call Activity và Local Variables
Call Activity
Khi một quy trình gọi một quy trình khác (sub-process) thông qua Call Activity, các biến không được tự động truyền qua. Bạn phải cấu hình ánh xạ (mapping) một cách tường minh. Điều này giúp đóng gói và kiểm soát dữ liệu, tránh việc quy trình con vô tình thay đổi dữ liệu của quy trình cha.
Video sau đây minh họa cách cấu hình việc truyền biến này.
Tutorial: Advanced BPMN Symbols and More (Video 5)
Video này từ Camunda giải thích cách cấu hình một Call Activity. Hãy chú ý đến phần thiết lập truyền biến.
Hãy xem từ 15:36 đến 17:31. Người hướng dẫn sẽ chỉ cho bạn tab 'Variables' trong Properties Panel của một Call Activity. Chú ý cách anh ấy sử dụng tùy chọn 'all' cho cả Input và Output Mapping để truyền tất cả các biến từ quy trình cha sang quy trình con và ngược lại. Trong thực tế, bạn thường sẽ chỉ định các biến cụ thể cần truyền.
Local Variables
Đôi khi, bạn cần một biến chỉ tồn tại trong phạm vi của một task duy nhất và không muốn nó "lọt" ra scope toàn cục. Đây là lúc local variables phát huy tác dụng.
- Tạo biến local:
execution.setVariableLocal("myLocalVar", "someValue"); - Tác dụng:
myLocalVarsẽ chỉ tồn tại trong execution hiện tại (ví dụ: một lần lặp trong multi-instance task) và không ghi đè lên biến cùng tên ở cấp độ cao hơn.
Bạn có thể đọc thêm về Local Variables và Variable Shadowing trong bài viết LINK đã được giới thiệu ở đầu bài học để hiểu rõ hơn.
Tổng kết
Trong bài học này, bạn đã khám phá cơ chế trung tâm giúp dữ liệu "chảy" trong một quy trình Camunda.
Những điểm chính cần nhớ:
- Phạm vi toàn cục (Global Scope): Theo mặc định, các biến có phạm vi trên toàn bộ process instance, cho phép chúng được truyền tự động giữa các task.
- Truy cập biến: Bạn có thể đọc và ghi biến trong các Script Task (qua
executionhoặc tên biến) và Service Task (quaDelegateExecution). - User Task: Là một điểm quan trọng để thu thập dữ liệu từ người dùng và lưu vào các biến quy trình.
- Kiểm soát luồng dữ liệu: Với các
Call Activity, bạn phải ánh xạ biến một cách tường minh. Vớilocal variables, bạn có thể giới hạn phạm vi của biến trong một task cụ thể.
Xem trước bài học tiếp theo
Bây giờ bạn đã biết cách tạo và truyền dữ liệu. Nhưng việc liên tục viết code Java hoặc Javascript chỉ để thực hiện các phép so sánh đơn giản (amount > 1000) có vẻ hơi cồng kềnh. Liệu có cách nào để sử dụng dữ liệu này trực tiếp trên sơ đồ BPMN không?
Câu trả lời là có. Trong bài học tiếp theo, chúng ta sẽ khám phá Expression Language (JUEL), một công cụ mạnh mẽ cho phép bạn đọc và xử lý dữ liệu ngay trong các thuộc tính của mô hình BPMN, đặc biệt là trong các điều kiện của gateway.