12. Çoklu Örnek (Multi-Instance) Görevleri
Çoklu örnek görevler, bir öğe koleksiyonuna (liste) dayalı olarak paralel (aynı anda) veya ardışık (sırayla) olmak üzere birden çok kez yürütülür. Bu özellik, aynı görevin birden fazla kullanıcı tarafından tamamlanması gereken senaryolar (örneğin anketler, paralel onaylar) için oldukça faydalıdır.
Nasıl Çalışır
Birden fazla görev örneği oluşturmak için bir <userTask> öğesinin içine <multiInstanceLoopCharacteristics> ekleyin:
| Nitelik | Açıklama |
|---|---|
isSequential |
false = paralel (hepsi aynı anda), true = ardışık (biri diğerinden sonra) |
flowable:collection |
Üzerinde dönülecek (iterate) listeyi içeren değişken |
flowable:elementVariable |
Her bir döngüdeki (iteration) geçerli öğenin değişken adı |
Örnek: Bir Roldeki Tüm Kullanıcılar İçin Paralel Görev
<!-- Step 1: Fetch users with ROLE_REVIEWER into a list variable -->
<serviceTask id="fetchReviewers" flowable:delegateExpression="${assignUsersByRole}">
<extensionElements>
<flowable:field name="roleName" stringValue="ROLE_REVIEWER"/>
<flowable:field name="outputVariable" stringValue="reviewerList"/>
</extensionElements>
</serviceTask>
<sequenceFlow sourceRef="fetchReviewers" targetRef="parallelReview"/>
<!-- Step 2: Create a task for EACH reviewer in the list -->
<userTask id="parallelReview" name="Review Document" flowable:assignee="${reviewer}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="reviewerList"
flowable:elementVariable="reviewer"/>
<extensionElements>
<activiti:formProperty id="feedback" name="Your Feedback" type="string" required="true">
<activiti:value id="type" name="textarea"/>
</activiti:formProperty>
</extensionElements>
</userTask>
Davranış: Eğer
reviewerList3 kullanıcı içeriyorsa, her bir incelemeci (reviewer) için bir tane olmak üzere üç paralel görev oluşturulur. Süreç, ancak tüm örnekler tamamlandıktan sonra devam eder.
Ardışık (Sequential) Çoklu Örnek
Ardışık yürütme (her seferinde bir görev) için isSequential="true" olarak ayarlayın:
<userTask id="sequentialApproval" name="Approval" flowable:assignee="${approver}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="approverList"
flowable:elementVariable="approver"/>
</userTask>
Kullanım Alanı: Bir sonraki seviyenin görevi görebilmesi için her seviyenin onaylaması gereken komuta zinciri onayları (Chain of command approvals).
Sabit Kodlanmış Kullanıcılar İçin Paralel Görev (Script Task)
Tasarım zamanında görev alacak kullanıcıları tam olarak biliyorsanız ve her birinin paralel bir görev almasını istiyorsanız, listeyi oluşturmak için bir Script Görevi (Script Task) kullanın:
<!-- Step 1: Build the user list -->
<scriptTask id="buildList" name="Build User List" scriptFormat="super-js">
<script>
var list = new java.util.ArrayList();
list.add('jane@workingflow.com');
list.add('jess@workingflow.com');
list.add('jade@workingflow.com');
execution.setVariable('reviewerList', list);
</script>
</scriptTask>
<sequenceFlow sourceRef="buildList" targetRef="parallelTask"/>
<!-- Step 2: Create a task for EACH user in the list -->
<userTask id="parallelTask" name="Review Document" flowable:assignee="${reviewer}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="reviewerList"
flowable:elementVariable="reviewer"/>
<extensionElements>
<activiti:formProperty id="feedback" name="Your Feedback" type="string" required="true">
<activiti:value id="type" name="textarea"/>
</activiti:formProperty>
</extensionElements>
</userTask>
Ne zaman kullanılmalı: Görevi tam olarak hangi kullanıcıların alması gerektiğini biliyorsanız (ör. sabit bir inceleme kurulu).
assignUsersByRoletemsilcisine (delegate) gerek yoktur — Script Görevi listeyi doğrudan oluşturur.
Onay Kutusu Seçiminden Paralel Görev (Script Task)
Önceki bir görev kullanıcıları seçmek için bir users_* onay kutusu alanı (çoklu seçim) kullanıyorsa, seçilen değerler virgülle ayrılmış dize (string) olarak saklanır (örneğin "jane@work.com,jess@work.com"). Çoklu örnek bir java.util.List gerektirdiğinden, dizeyi dönüştürmek için bir Script Görevi kullanılır:
<!-- Previous task has a users_* checkbox field -->
<userTask id="pickReviewers" name="Select Reviewers" flowable:assignee="${initiator}">
<extensionElements>
<activiti:formProperty id="users_reviewers" name="Select Reviewers" type="enum" required="true"/>
</extensionElements>
</userTask>
<sequenceFlow sourceRef="pickReviewers" targetRef="splitUsers"/>
<!-- Step 1: Convert comma-separated string to a list -->
<scriptTask id="splitUsers" name="Prepare Reviewer List" scriptFormat="super-js">
<script>
var selected = execution.getVariable('users_reviewers');
var list = new java.util.ArrayList(java.util.Arrays.asList(selected.split(',')));
execution.setVariable('reviewerList', list);
</script>
</scriptTask>
<sequenceFlow sourceRef="splitUsers" targetRef="parallelReview"/>
<!-- Step 2: Create a task for EACH selected user -->
<userTask id="parallelReview" name="Review" flowable:assignee="${reviewer}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="reviewerList"
flowable:elementVariable="reviewer"/>
<extensionElements>
<activiti:formProperty id="comment" name="Your Comment" type="string" required="true">
<activiti:value id="type" name="textarea"/>
</activiti:formProperty>
</extensionElements>
</userTask>
Önemli nokta:
users_*öneki virgülle ayrılmış bir dize saklar, ancakflowable:collectionbirjava.util.Listgerektirir. Script Görevijava.util.Arrays.asList(selected.split(','))ile bu aralığı kapatır.
📦 Değişken Toplama (Variable Aggregation) — Paralel Sonuçları Toplama
Paralel çoklu örnek görevleri yanıtları (örneğin anketler, geri bildirim turları) topladığında, Değişken Toplama (Variable Aggregation), her kullanıcının girdisini tek bir JSON dizisi (JSON array) değişkeninde otomatik olarak toplayan yerel bir Flowable özelliğidir. list türü ipucuyla birleştirildiğinde, toplanan sonuçlar (her yanıt için bir tane olmak üzere) ayrı salt okunur (read-only) etiketler olarak görüntülenir.
Parçalar Nasıl Bir Araya Geliyor
Bu desen 4 bağlantılı unsurdan oluşur. İşte tam açıklamalı örnek:
<!-- ──────────────────────────────────────────────────────────────── -->
<!-- STEP 1: Multi-Instance User Task with Variable Aggregation -->
<!-- -->
<!-- flowable:assignee="${user}" -->
<!-- └─ "user" AŞAĞIDAKİ elementVariable İLE EŞLEŞMELİDİR -->
<!-- -->
<!-- KESİN KISITLAMA: <extensionElements> bir <userTask> içinde -->
<!-- MUTLAKA <multiInstanceLoopCharacteristics> 'ten ÖNCE GELMELİDİR.-->
<!-- Bu bir XSD doğrulama kuralıdır — yerlerini değiştirmek -->
<!-- dağıtım (deployment) hatasına neden olur. -->
<!-- ──────────────────────────────────────────────────────────────── -->
<userTask id="giveArgument" name="Share Your Thoughts" flowable:assignee="${user}">
<extensionElements>
<!-- Form fields for each parallel task instance -->
<activiti:formProperty id="argument" name="Your Argument" type="string" required="true">
<activiti:value id="type" name="textarea"/>
</activiti:formProperty>
</extensionElements>
<!-- ──────────────────────────────────────────────────────────── -->
<!-- Çoklu Örnek Yapılandırması -->
<!-- -->
<!-- flowable:collection="userList" -->
<!-- └─ java.util.List değişkeni (önceki adımda oluşturulur) -->
<!-- -->
<!-- flowable:elementVariable="user" -->
<!-- └─ assignee="${user}" içinde kullanılan listedeki her öğe -->
<!-- ──────────────────────────────────────────────────────────── -->
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="userList"
flowable:elementVariable="user">
<!-- ────────────────────────────────────────────────────────── -->
<!-- Değişken Toplama (userTask içinde DEĞİL, multiInstance içinde) -->
<!-- -->
<!-- target="allArguments" -->
<!-- └─ Ortaya çıkan JSON dizisi değişkeninin adı. -->
<!-- Tüm görevler tamamlandığında, bu değişken şunları tutar: -->
<!-- [{"argument":"...","username":"..."}, {...}, ...] -->
<!-- -->
<!-- <variable source="argument" target="argument"/> -->
<!-- └─ source: her görevden toplanacak form alanı ID'si -->
<!-- └─ target: ortaya çıkan JSON nesnesindeki anahtar (key) adı-->
<!-- -->
<!-- <variable sourceExpression="${user}" target="username"/> -->
<!-- └─ sourceExpression: her örnek için değerlendirilir -->
<!-- └─ KURAL: target="username" ayrılmış bir anahtardır. -->
<!-- Oluşturucu bu anahtarı arar, e-postayı tam isme -->
<!-- dönüştürür ve etiket başlığında kullanır. -->
<!-- E-posta değeri ekrandan gizlenir. -->
<!-- ────────────────────────────────────────────────────────── -->
<extensionElements>
<flowable:variableAggregation target="allArguments">
<variable source="argument" target="argument"/>
<variable sourceExpression="${user}" target="username"/>
</flowable:variableAggregation>
</extensionElements>
</multiInstanceLoopCharacteristics>
</userTask>
Adım 2: ArrayNode'u Dizeye (String) Dönüştürme (Zorunlu)
Flowable, toplanan değişkeni bir Jackson ArrayNode nesnesi olarak saklar, ancak StringFormType (type="string" olan form özellikleri tarafından kullanılır) bir String bekler. Tek satırlık bir Script Görevi (Script Task) bunu dönüştürür:
<scriptTask id="convertResults" name="Prepare Results" scriptFormat="super-js">
<script>
var raw = execution.getVariable('allArguments');
execution.setVariable('allArguments', raw.toString());
</script>
</scriptTask>
Bu neden gerekli? Bu dönüştürme işlemi yapılmadan inceleme görevini açmak
ClassCastException: ArrayNode cannot be cast to Stringhatası fırlatır. Bu, Workingflow ile ilgili bir sorun değil, Flowable form motorunun bir kısıtlamasıdır.
Adım 3: list Türü İpucu ile Görüntüleme
list türü ipucu, FormRenderingService'e JSON dizisini çoklu etiketlere genişletmesini söyler:
<userTask id="reviewAll" name="Review All" flowable:assignee="${initiator}">
<extensionElements>
<activiti:formProperty id="allArguments" name="Employee Argument" type="string" writable="false">
<activiti:value id="type" name="list"/>
</activiti:formProperty>
</extensionElements>
</userTask>
Oluşturucunun yaptığı işlem:
- JSON dizisini ayrıştırır:
[{"argument":"Harika fikir","username":"jane@work.com"}, ...] - Her giriş için
usernameanahtarının (key) mevcut olup olmadığını kontrol eder. - Bulunursa → kullanıcının tam adını veritabanından arar → başlık
Employee Argument (Jane Doe)olur ve e-posta değerden çıkarılır. usernameanahtarı yoksa → başlıkEmployee Argument (1),(2)vb. olarak atanır.- Kalan alanlar etiket içeriği olarak gösterilir.
Sonuç (4 kullanıcı yanıt verdi):
Employee Argument (Jenn Employee) → "I fully support this idea"
Employee Argument (Jill Employee) → "Needs more research first"
Employee Argument (Joye Employee) → "Let's do a pilot run"
Employee Argument (Janette Employee) → "Approved, go ahead"
Temel Kısıtlamalar Özeti
| Kısıtlama | Nedeni |
|---|---|
<extensionElements>, <multiInstanceLoopCharacteristics> öğesinden ÖNCE gelmelidir |
XSD doğrulama kuralı — sıralamanın ters olması dağıtımı başarısız kılar |
<flowable:variableAggregation>, <multiInstanceLoopCharacteristics> içinde olmalıdır |
Görevin kendisine değil, çoklu örnek döngüsüne (multi-instance loop) aittir |
<variable> içindeki source, form alanının id'si ile eşleşmelidir |
Flowable gönderilen değeri buradan okur |
flowable:elementVariable, flowable:assignee="${...}" ile eşleşmelidir |
Döngü değişkeni her görevi doğru kullanıcıya atar |
| ArrayNode → String dönüştürmesi yapan Script Görevi | Flowable'ın StringFormType'ı ArrayNode nesnelerini işleyemez |
list tür ipucu standart type="string" kullanır |
Flowable türü string olarak kalır — sadece işleme (rendering) ipucu değişir |
target="username" bir kuraldır |
Oluşturucu (renderer), etiket başlığı için bu değeri tam bir isimle değiştirir |
Referans Uygulama: Tüm bu özelliklerin birlikte kullanıldığı tam bir çalışan örnek için
idea_poll.bpmnsürecini inceleyin. Aynı zamandageneratePdfkullanımını da gösterir — tüm tartışmalar toplandıktan sonra biçimlendirilmiş bir PDF özeti oluşturulur ve son inceleme görevinde indirmeye sunulur.
📝 Dinamik Öğe Toplama — Metin Alanından Listeye Deseni (Textarea-to-List Pattern)
Değişken Toplama (Variable Aggregation) birden fazla kullanıcıdan veri toplarken (her kullanıcıdan bir öğe), tek bir kullanıcının değişken sayıda öğe — masraf kalemleri, kontrol listesi öğeleri, eylem noktaları vb. — girmesi gereken durumlar olabilir. BPMN formları doğası gereği sabittir (bir özellik = bir alan), ancak bu desen yalnızca mevcut özellikleri kullanarak dinamik veri toplama işlemini başarır.
Nasıl Çalışır
Desen, üç mevcut yeteneği birbirine bağlar:
Textarea (kullanıcı N öğe girer) → Script Görevi (JSON dizisine ayırır) → list tür ipucu (N etiket gösterir)
Yeni sözleşmelere, yeni tür ipuçlarına, platform değişikliklerine gerek yoktur — sadece textarea + Script Görevi (Script Task) + list kombinasyonunun yaratıcı bir kullanımıdır.
Adım 1: Öğeleri Bir Metin Alanı (Textarea) ile Toplama
Kullanıcıya beklenen biçimi söylemek için bir textarea tür ipucu ve <documentation> notu kullanın:
<userTask id="enterItems" name="Enter Expense Items" flowable:assignee="${initiator}">
<documentation>Enter each expense on a new line (or separate with ;). Format: item name, amount. Example: Taxi to airport, 25</documentation>
<extensionElements>
<activiti:formProperty id="expenseLines" name="Expense Items" type="string" required="true">
<activiti:value id="type" name="textarea"/>
</activiti:formProperty>
</extensionElements>
</userTask>
Kullanıcı dilediği sayıda satırı serbestçe yazar:
Taxi to airport, 25
Hotel 2 nights, 300
Client dinner, 85
Veya tek bir satırda noktalı virgül ile: Taxi, 25; Hotel, 300; Dinner, 85
Adım 2: Bir Script Görevi ile Ayrıştırma
Bir Script Görevi, metni yapılandırılmış JSON'a böler. Bu örnek "öğe adı, miktar" çiftlerini ayrıştırır:
<scriptTask id="parseItems" name="Parse Items" scriptFormat="super-js">
<script><![CDATA[
var raw = execution.getVariable('expenseLines');
var lines = raw.split(/[;\n]/);
var items = [];
var total = 0;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line === '') continue;
var commaIdx = line.lastIndexOf(',');
var itemName;
var amount;
if (commaIdx > 0) {
itemName = line.substring(0, commaIdx).trim();
amount = parseFloat(line.substring(commaIdx + 1).trim());
if (isNaN(amount)) { itemName = line; amount = 0; }
} else {
itemName = line;
amount = 0;
}
items.push({"item": itemName, "amount": amount});
total += amount;
}
var jsonData = JSON.stringify(items);
execution.setVariable('expenseData', jsonData);
execution.setVariable('expenseTotal', wf.formatCurrency(total, '$'));
]]></script>
</scriptTask>
Önemli: Java koleksiyonları (
java.util.ArrayList,java.util.HashMap) DEĞİL, JavaScript dizileri ve nesneleri ([],{}) kullanın. GraalJS'inJSON.stringify()fonksiyonu Java nesnelerini serileştiremez — çıktı olarak boş{}üretir.
Adım 3: list Tür İpucu İle Görüntüleme
Ayrıştırılan JSON dizisi doğrudan list türü ipucunu besler — Değişken Toplama (Variable Aggregation) tarafından kullanılanla aynıdır:
<userTask id="reviewItems" name="Review Expenses" flowable:candidateGroups="ROLE_MANAGER">
<extensionElements>
<activiti:formProperty id="expenseData" name="Expense Items" type="string" writable="false">
<activiti:value id="type" name="list"/>
</activiti:formProperty>
<activiti:formProperty id="expenseTotal" name="Total Amount" type="string" writable="false">
<activiti:value id="type" name="label"/>
</activiti:formProperty>
</extensionElements>
</userTask>
Sonuç (3 öğe girildi):
Expense Items (1) → Taxi to airport • 25
Expense Items (2) → Hotel 2 nights • 300
Expense Items (3) → Client dinner • 85
Total Amount → $410.00
İsteğe Bağlı: Bir PDF Raporu Oluşturma
Ayrıştırılmış JSON ayrıca wf.toHtmlTableWithHeaders() ve ${generatePdf} ile de çalışır:
<scriptTask id="buildHtml" name="Build Report HTML" scriptFormat="super-js">
<script>
var jsonData = execution.getVariable('expenseData');
execution.setVariable('tableHtml',
wf.toHtmlTableWithHeaders(jsonData, 'item,amount', 'Expense Item,Amount ($)'));
</script>
</scriptTask>
<serviceTask id="genPdf" flowable:delegateExpression="${generatePdf}">
<extensionElements>
<flowable:field name="outputVariable" stringValue="expenseReport"/>
<flowable:field name="template">
<flowable:string><![CDATA[
<html><body>
<h1>Expense Report</h1>
${tableHtml}
<p><strong>Total: ${expenseTotal}</strong></p>
</body></html>
]]></flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
Karşılaştırma: Değişken Toplama ve Metin Alanı (Textarea) Deseni
| Değişken Toplama | Metin Alanı Deseni | |
|---|---|---|
| Veri Kaynağı | Birden çok kullanıcı, her biri tek öğe | Tek kullanıcı, birden çok öğe |
| BPMN Mekanizması | Çoklu Örnek (Multi-instance) + variableAggregation |
Metin alanı (Textarea) + Script Görevi |
| Çıktı Biçimi | JSON dizisi (otomatik) | JSON dizisi (Script Görevi oluşturur) |
| Görüntüleme | list türü ipucu |
list türü ipucu (aynı!) |
| PDF Desteği | wf.toHtmlTable*() |
wf.toHtmlTable*() (aynı!) |
| Gerekli Yeni Özellikler | Yok | Yok |
Her iki desen de aynı JSON dizisi biçimini oluşturur, aynı list türü ipucu tarafından tüketilir ve aynı wf.* yardımcı işlevleriyle (helpers) çalışır. Tek fark, veriyi kimin sağladığıdır.
Referans Uygulama: Ayrıştırma, liste gösterimi, yönetici onayı/reddi/revizyon döngüsü ve PDF oluşturma süreçlerinin tamamının yer aldığı çalışan bir örnek için
expense_report_dynamic.bpmnsürecine bakın.