Skip to content

مستندات پایگاه داده (Database Documentation) ​

مرجع اصلی ساختار و اسکیمای پایگاه داده، کلاس PostgresStore در مسیر src/TaskFlow.Infrastructure/Persistence/PostgresStore.cs است. عملیات مهاجرت (Migration) به‌صورت خودکار در نخستین اتصال انجام می‌پذیرد و نیازی به ابزار مهاجرت خارجی یا فایل‌های مجزای SQL وجود ندارد.

این مستند ساختار جداول، ستون‌ها، شاخص‌ها (Indexes)، الگوهای کوئری و محدودیت‌های عملیاتی را به تفصیل تشریح می‌کند.


۱. نقش و ساختار پایگاه داده ​

این سیستم نیازی به ذخیره‌سازی دیاگرام‌های پیچیدهٔ گرافیکی ندارد و در Postgres صرفاً سه موجودیت اصلی پایدار نگهداری می‌شوند:

موجودیتنام جدولمفهوم و نقش کسب‌وکاری
تعریف فرایندdefinitionsتعریف نوع فرایند (مانند purchase یا employeeTermination)
نمونهٔ اجراinstancesرکورد یک اجرای مستقل (شروع فرایند یا ثبت ارجاع)
وظیفهٔ کارتابلtasksوظیفهٔ ارجاع‌شده به شخص یا گروه

اطلاعات هویتی کاربران و عضویت در گروه‌ها در دیتابیس ذخیره نمی‌شوند. اگر دایرکتوری پیکربندی نشود، شناسه‌ها مات هستند (OpenDirectory). در غیر این صورت از IDirectory (StaticDirectory یا LDAP/SSO) تأمین می‌گردند.

دو پیاده‌سازی برای رابط IStore فراهم شده است:

پیاده‌سازیشرط فعال‌سازیماندگاری داده‌ها
PostgresStoreتنظیم بودن متغیر DATABASE_URLدائمی و پایدار
MemoryStoreخالی بودن متغیر DATABASE_URLموقت در حافظه (با توقف برنامه پاک می‌شود)

در زمان راه‌اندازی سرور (Program.cs) پیاده‌سازی مناسب به‌صورت خودکار انتخاب می‌شود؛ لایهٔ منطق کسب‌وکار (Engine) هیچ وابستگی مستقیمی به نوع ذخیره‌ساز ندارد.


۲. نحوهٔ اتصال و مهاجرت خودکار ​

رشتهٔ اتصال (Connection String) ​

متغیر محیطی:

DATABASE_URL=postgres://workflow:workflow@postgres:5432/workflow?sslmode=disable

تنظیمات پیش‌فرض سرویس در Docker Compose:

پارامترمقدار در Docker Compose
تصویر دیتابیسpostgres:16-alpine
نام کاربریworkflow
کلمهٔ عبورworkflow
نام پایگاه دادهworkflow
پورت سیستم میزبان5432
حجم ذخیره‌سازی (Volume)pgdata

در صورت عدم دسترسی اولیه به Postgres در هنگام شروع سرویس، سرور تا ۳۰ بار با فواصل ۱ ثانیه‌ای تلاش مجدد (Retry) انجام می‌دهد.

سازوکار مهاجرت خودکار (Migration) ​

متد PostgresStore.Open(dsn) در اولین اجرا بلافاصله متد Migrate را فراخوانی می‌کند:

۱. اجرای دستورات CREATE TABLE IF NOT EXISTS و CREATE INDEX IF NOT EXISTS برای ساخت جداول و شاخص‌ها.
۲. اجرای دستورات ALTER TABLE … ADD COLUMN IF NOT EXISTS برای اطمینان از وجود ستون‌های الحاقی در نسخه‌های ارتقایافته.

این عملیات کاملاً پایا (Idempotent) بوده و اجرای مکرر آن روی دیتابیس خالی یا پایگاه‌های دادهٔ قدیمی بدون ریسک است. دستورات DDL در قالب یک تراکنش واحد سراسری اجرا نمی‌شوند؛ بلکه هر بخش با یک دستور ExecuteNonQuery مستقل به اجرا درمی‌آید.


۳. مدل مفهومی و روابط موجودیت‌ها ​

سلسله‌مراتب داده از سازمان تا وظیفه:

tenant (tenant_id)
  └── definition
        └── root instance       parent_instance_id = ''
              └── child instance   one row per AssignTo
                    └── task(s)      inbox assignee: user or group
  • definition: نوع فرایند (مثلاً purchase)
  • root instance: خروجی متد Start
  • child instance: هر عملیات AssignTo یک ردیف جدید
  • task: وظیفهٔ کارتابل برای کاربر یا گروه

قواعد ساختاری مهم:

  • متد Start هیچ تسکی ایجاد نمی‌کند؛ صرفاً تعریف فرایند (در صورت نبود) و یک نمونهٔ اجرای ریشه می‌سازد.
  • هر عملیات AssignTo یک نمونهٔ اجرای جدید ایجاد می‌کند؛ در صورت ارسال parentInstanceId، این نمونه به ریشه متصل می‌گردد.
  • وظایف (tasks) مستقیماً به نمونهٔ اجرای ارجاع متصل هستند.
  • جهت دریافت کلیهٔ وظایف یک فرایند، کوئری جستجو هر دو فیلد instance_id و parent_instance_id را بررسی می‌کند.
mermaid
erDiagram
  definitions ||--o{ instances : "definition_id / definition_key"
  instances ||--o{ instances : "parent_instance_id"
  instances ||--o{ tasks : "instance_id"
  instances ||--o{ tasks : "parent_instance_id (denormalized)"

  definitions {
    text id PK
    text tenant_id
    text key
    text name
    int version
    jsonb graph
    boolean published
    timestamptz created_at
  }

  instances {
    text id PK
    text tenant_id
    text definition_id
    text definition_key
    text parent_instance_id
    text status
    jsonb vars
    text started_by
    int version
    timestamptz created_at
    timestamptz updated_at
  }

  tasks {
    text id PK
    text tenant_id
    text instance_id
    text parent_instance_id
    text definition_key
    text node_id
    text token_id
    text title
    text note
    text assignee_kind
    text assignee_id
    text claimed_by
    text assigned_by
    text status
    text group_mode
    text return_reason
    timestamptz created_at
    timestamptz updated_at
    timestamptz completed_at
  }

نکته: به‌منظور حفظ سادگی و کارایی، قید کلید خارجی فیزیکی (FOREIGN KEY) در سطح پایگاه داده تعریف نشده است و یکپارچگی ارجاعی در سطح لایهٔ Application تضمین می‌گردد.


۴. شناسه‌ها، زمان، و جداسازی سازمان‌ها ​

فرمت شناسه‌ها (Identifiers) ​

شناسه‌ها توسط متد Ids.New() با تولید ۱۶ بایت تصادفی امن و تبدیل آن به رشتهٔ هگزادسیمال کوچک ۳۲ کاراکتری تولید می‌شوند. کلیهٔ کلیدهای اصلی (definitions.id، instances.id و tasks.id) از این نوع هستند و از توالی‌های عددی (Sequence) یا نوع UUID بومی دیتابیس استفاده نمی‌شود.

مدیریت زمان (Timestamps) ​

تمام ستون‌های زمانی از نوع TIMESTAMPTZ بوده و بر پایهٔ ساعت هماهنگ جهانی (UTC) ذخیره و بازیابی می‌شوند.

جداسازی داده‌های سازمانی (tenant_id) ​

هر سه جدول دارای ستون tenant_id TEXT NOT NULL DEFAULT 'default' هستند:

  • مقادیر خالی یا تهی به‌طور خودکار به 'default' تبدیل می‌شوند (Tenant.Normalize).
  • در لایهٔ REST از طریق هدر X-Tenant-Id دریافت می‌گردد.
  • تفکیک داده‌ها در سطح لایهٔ منطق برنامه (Application) مدیریت می‌شود.
  • در صورت عدم تطابق سازمان در زمان دریافت اطلاعات، خطای ForbiddenTenant صادر می‌شود.
  • ایندکس‌های جستجو شامل فیلد tenant_id هستند تا سرعت بازیابی بهینه باشد.

۵. جدول تعاریف فرایند (definitions) ​

کاتالوگ انواع فرایندها در این جدول نگهداری می‌شود.

نام ستوننوع دادهمقدار پیش‌فرضتوضیحات و کاربرد
idTEXT PK—شناسهٔ یکتای تعریف
tenant_idTEXT NOT NULL'default'شناسهٔ سازمان
keyTEXT NOT NULL—کلید شناسهٔ فرایند (مانند purchase)
nameTEXT NOT NULL''نام نمایشی فرایند (در صورت خالی بودن برابر key تنظیم می‌شود)
versionINT NOT NULL1شمارهٔ نسخه (رزرو برای توسعه‌های آتی)
graphJSONB NOT NULL'{}'دیاگرام ساختار فرایند (رزرو برای توسعه‌های آتی)
publishedBOOLEAN NOT NULLTRUEوضعیت انتشار فرایند
created_atTIMESTAMPTZ NOT NULLNOW()زمان ایجاد رکورد

دستور ثبت (Insert / Update) ​

sql
INSERT INTO definitions (id, tenant_id, key, name, version, graph, published, created_at)
VALUES ($1, $2, $3, $4, 1, '{}'::jsonb, TRUE, $5)
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;

۶. جدول نمونه‌های اجرا (instances) ​

هر ردیف نمایندهٔ یک نمونهٔ اجرایی (شروع ریشه یا یک ارجاع جدید) است.

نام ستوننوع دادهمقدار پیش‌فرضتوضیحات و کاربرد
idTEXT PK—شناسهٔ نمونهٔ اجرا (instanceId)
tenant_idTEXT NOT NULL'default'شناسهٔ سازمان
definition_idTEXT NOT NULL—شناسهٔ تعریف فرایند مربوطه
definition_keyTEXT NOT NULL—کلید تعریف فرایند جهت بهینه‌سازی کوئری‌ها بدون نیاز به JOIN
parent_instance_idTEXT NOT NULL''شناسهٔ نمونهٔ ریشه (برای نمونه‌های ریشه برابر رشتهٔ خالی است)
statusTEXT NOT NULL—وضعیت اجرا (running یا completed)
varsJSONB NOT NULL'{}'پارامترها و متغیرهای فرایند به‌صورت JSON
started_byTEXT NOT NULL—شناسهٔ ایجادکننده یا ارجاع‌دهنده
versionINT NOT NULL1شمارهٔ نسخهٔ رکورد
created_atTIMESTAMPTZ NOT NULL—زمان شروع نمونهٔ اجرا
updated_atTIMESTAMPTZ NOT NULL—زمان آخرین به‌روزرسانی

وضعیت‌های اجرا (status) ​

  • running: فرایند در حال اجراست و کارهای ناتمام دارد.
  • completed: کلیهٔ وظایف ارجاع مربوطه یا کل درخت فرایند خاتمه یافته است.

ساختار درختی والد و فرزند ​

  • نمونهٔ ریشه: مقدار parent_instance_id برابر رشتهٔ خالی '' است.
  • نمونهٔ ارجاع: مقدار parent_instance_id برابر با شناسهٔ نمونهٔ ریشه است.
  • عملیات CompleteAndEnd با یافتن ریشه، تمام وظایف باز زیرمجموعه را لغو (cancelled) کرده و وضعیت تمام نمونه‌های زیرمجموعه را به completed تغییر می‌دهد.

۷. جدول وظایف کارتابل (tasks) ​

هر ردیف نشان‌دهندهٔ یک وظیفهٔ اختصاص‌یافته به کاربر یا گروه است.

نام ستوننوع دادهمقدار پیش‌فرضتوضیحات و کاربرد
idTEXT PK—شناسهٔ یکتای وظیفه (taskId)
tenant_idTEXT NOT NULL'default'شناسهٔ سازمان
instance_idTEXT NOT NULL—شناسهٔ نمونهٔ ارجاع وابسته
parent_instance_idTEXT NOT NULL''شناسهٔ نمونهٔ ریشه جهت فیلتر آسان کل وظایف فرایند
definition_keyTEXT NOT NULL''کلید تعریف فرایند
node_idTEXT NOT NULL''شناسهٔ گره در گراف (رزرو)
token_idTEXT NOT NULL''شناسهٔ توکن اجرایی (رزرو)
titleTEXT NOT NULL''عنوان ارجاع و موضوع کار
noteTEXT NOT NULL''توضیحات یا یادداشت ثبت‌شده هنگام تکمیل
assignee_kindTEXT NOT NULL—نوع انتساب (user یا group)
assignee_idTEXT NOT NULL—شناسهٔ کاربر یا گروه منتسب
claimed_byTEXT NOT NULL''شناسهٔ کاربری که وظیفه را به خود اختصاص داده است
assigned_byTEXT NOT NULL''شناسهٔ کاربر ارجاع‌دهنده
statusTEXT NOT NULL—وضعیت وظیفه (open, claimed, done, cancelled)
group_modeTEXT NOT NULL''حالت گروهی (رزرو)
return_reasonTEXT NOT NULL''دلیل برگشت کار (رزرو)
created_atTIMESTAMPTZ NOT NULL—زمان تخصیص وظیفه
updated_atTIMESTAMPTZ NOT NULL—زمان آخرین تغییر وضعیت
completed_atTIMESTAMPTZ nullableNULLزمان دقیق تکمیل وظیفه

چرخهٔ حیات وضعیت وظیفه (status) ​

        claim                 complete
  open ──────► claimed ──────► done
    ▲             │
    └─ unclaim ───┘

  open / claimed ──completeAndEnd──► cancelled
وضعیتثابت معادل در کدتوضیحات
openTaskStatus.Openوظیفه تازه ایجاد شده و هنوز در حال انتظار است
claimedTaskStatus.Claimedوظیفهٔ گروهی توسط یک عضو رزرو و تحویل گرفته شده است
doneTaskStatus.Doneوظیفه با موفقیت انجام و تکمیل شده است
cancelledTaskStatus.Cancelledوظیفه با دستور پایان فرایند لغو گردیده است

۸. شاخص‌های پایگاه داده (Indexes) ​

تمامی شاخص‌ها به‌صورت CREATE INDEX IF NOT EXISTS ایجاد می‌گردند:

نام شاخصجدولستون‌هاهدف و کاربرد
tasks_instance_idxtasks(instance_id)بازیابی سریع وظایف متعلق به یک ارجاع
tasks_assignee_idxtasks(assignee_kind, assignee_id, status)بهینه‌سازی کوئری‌های کارتابل وظایف کاربر و گروه
tasks_parent_idxtasks(parent_instance_id)بازیابی وظایف مرتبط با کل درخت یک فرایند
instances_process_idxinstances(tenant_id, definition_key, parent_instance_id)دریافت فهرست اجراهای ریشه برای یک نوع فرایند
instances_initiator_idxinstances(tenant_id, started_by, parent_instance_id)دریافت فهرست فرایندهای آغازشده توسط کاربر

۹. الگوهای کوئری و متدهای لایهٔ داده ​

متد در رابط IStoreساختار کوئری معادل SQL
GetDefinitionByKeyWHERE tenant_id=$1 AND key=$2 ORDER BY created_at DESC LIMIT 1
GetInstanceWHERE id=$1
UpdateInstanceUPDATE instances SET status=$2, vars=$3::jsonb, updated_at=$4 WHERE id=$1
ListRootInstancesWHERE tenant_id=$1 AND definition_key=$2 AND parent_instance_id='' ORDER BY created_at DESC
ListRootInstancesByInitiatorWHERE tenant_id=$1 AND started_by=$2 AND parent_instance_id='' ORDER BY created_at DESC
ListChildInstancesWHERE parent_instance_id=$1 ORDER BY created_at
GetTaskWHERE id=$1
TransitionTaskقفل سطری با SELECT … FOR UPDATE و سپس UPDATE در تراکنش واحد
ListTasksاعمال فیلترهای پویا با ترتیب ORDER BY created_at

۱۰. مدیریت همزمانی در پایگاه داده ​

عملیات تغییر وضعیت تسک‌ها (شامل تکمیل، رزرو و لغو رزرو) از طریق متد TransitionTask انجام می‌شود. اگر وضعیت فعلی با وضعیت‌های مجاز مطابقت نداشته باشد، خطای NotOpen برگردانده می‌شود.

sql
BEGIN;
SELECT id, status, ... FROM tasks WHERE id = $1 FOR UPDATE;
UPDATE tasks SET status = $2, note = $3, updated_at = $4, completed_at = $5, claimed_by = $6 WHERE id = $1;
COMMIT;

دستور FOR UPDATE رکورد وظیفه را تا پایان تراکنش قفل می‌کند تا از پدیدهٔ Race Condition در اقدامات هم‌زمان جلوگیری شود.


۱۱. مواردی که در دیتابیس ذخیره نمی‌شوند ​

  • احراز هویت و جداول کاربران: از طریق رابط IDirectory تأمین می‌گردد.
  • کلیدهای امنیتی API: از متغیرهای محیطی (WF_API_KEYS) خوانده می‌شود و در دیتابیس ذخیره نمی‌گردد. خارج از محیط Development این متغیر اجباری است. این کلید باید فقط روی بک‌اند (BFF) بماند و از مرورگر / React ارسال نشود؛ شرح معماری: architecture.md.
  • جدول وقایع تاریخی (Audit Log): تاریخچهٔ گام‌به‌گام ذخیره نمی‌شود و صرفاً آخرین وضعیت در رکورد تسک (claimed_by, updated_at, completed_at) موجود است.
  • پیوست‌ها و فایل‌ها: دیتابیس صرفاً فراداده‌های متنی و JSON را نگهداری می‌کند.

۱۲. سناریوی عملی تغییرات داده‌ها ​

سناریو: کاربر Alice فرایند purchase را شروع کرده و کار را به گروه legal ارجاع می‌دهد؛ سپس Mortenaho وظیفه را تحویل گرفته و تکمیل می‌کند.

۱. شروع فرایند (Start) ​

یک رکورد در جدول instances درج می‌شود (تسک ساخته نمی‌شود و وضعیت مفهومی اجرا notStarted است):

idparent_instance_idstatusstarted_byvars
root_1''runningsara{"amount": 150000000}

۲. تخصیص کار به گروه حقوقی (AssignTo) ​

یک رکورد فرزند در جدول instances و یک رکورد تسک در جدول tasks ایجاد می‌شود:

جدولidinstance_idparent_instance_idassignee_kindassignee_idstatus
instancesref_1—root_1——running
taskstask_1ref_1root_1grouplegalopen

کلیهٔ اعضای گروه حقوقی (مثلاً Mortenaho و Cara) تسک را در کارتابل خود مشاهده می‌کنند.

۳. تحویل گرفتن وظیفه توسط Mortenaho (Claim) ​

فیلد status به claimed و claimed_by به mortenaho تغییر می‌یابد. تسک از کارتابل Cara خارج شده و چنانچه وی تلاش به کلیم کند، خطای 409 Conflict دریافت خواهد کرد.

۴. تکمیل وظیفه توسط Mortenaho (Complete) ​

وضعیت تسک به done تغییر کرده و مقدار completed_at ثبت می‌شود. نمونهٔ اجرای ارجاع (ref_1) به دلیل تکمیل تمام وظایفش به وضعیت completed منتقل می‌گردد؛ در حالی که نمونهٔ ریشه (root_1) تا زمان دستور پایان همچنان در وضعیت running باقی می‌ماند.


۱۳. راهنمای عملیات و نگهداری ​

موضوعتوصیه و دستورالعمل
تهیهٔ نسخهٔ پشتیبان (Backup)تهیهٔ پشتیبان از Volume مربوط به pgdata یا اجرای استاندارد دستور pg_dump
تست‌های یکپارچگی پایگاه دادهکلاس PostgresStoreTests در صورت تنظیم متغیر DATABASE_URL اجرا می‌شود
امنیت ارتباطات پایگاه دادهدر محیط‌های تولیدی (Production) فعال‌سازی رمزنگاری TLS/SSL و استفاده از کلمات عبور پیچیده الزامی است

کوئری‌های کاربردی برای پایش عملیاتی:

نمونه‌های اجرای ریشه برای یک نوع فرایند:

sql
SELECT id, status, started_by, created_at
FROM instances
WHERE tenant_id = 'default'
  AND definition_key = 'purchase'
  AND parent_instance_id = '';

وظایف باز یک کاربر (فردی و رزروشده):

sql
SELECT id, title, status, assignee_kind, assignee_id
FROM tasks
WHERE tenant_id = 'default'
  AND status IN ('open', 'claimed')
  AND (
    (assignee_kind = 'user' AND assignee_id = 'mortenaho')
    OR claimed_by = 'mortenaho'
  );

ساختار درختی یک فرایند (ریشه و ارجاع‌های فرزند):

sql
SELECT id, parent_instance_id, status, started_by, created_at
FROM instances
WHERE id = :root_id OR parent_instance_id = :root_id;

۱۴. پیوند با سایر مستندات ​

TaskFlow — موتور گردش کار WorkFlowEngine