request-lifecycle.md 12.2 KB

The metadata-driven request lifecycle

The diagram you'll come back to. Every metadata-driven page in xly follows this same flow; once you've internalised it, the rest of the framework is variations on a theme.

The flow

┌──────────────────────────────────────────────────────────────────────┐
│  Browser                                                             │
│    1. Loads SPA shell at any URL (the server returns the same shell  │
│       for every path; gdsroute is a *client-side* sidebar/deep-link  │
│       whitelist, not a server-side 404 gate)                         │
│    2. User clicks a sidebar item or navigates within the SPA         │
│    3. SPA decides which module to load → calls /business/...         │
└──────────────────────────────────────────────────────────────────────┘
              │
              │  GET /xlyEntry/business/getModelBysId/{sModelsId}
              │      ?sModelsId={sModelsId}
              │  (the module's id appears in BOTH path and query —
              │   the controller binds the path variable, but the
              │   service reads sModelsId from the @RequestParam map,
              │   so the SPA must include it in the query string too)
              ▼
┌──────────────────────────────────────────────────────────────────────┐
│  xlyEntry — BusinessBaseController.getModelBysId()                   │
│                                                                      │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │  RequestAddParamUtil.addParams(params, userInfo)             │   │
│   │     → sBrandsId, sSubsidiaryId, sUserId, sLanguage, …        │   │
│   │       (16 keys total — see runtime.md)                       │   │
│   │     → tenant scope is now AVAILABLE for any downstream query │   │
│   │       that wants it. Framework-metadata reads filter by      │   │
│   │       form-id only; per-tenant overlays + business data      │   │
│   │       reads filter by sBrandsId/sSubsidiaryId.               │   │
│   └──────────────────────────────────────────────────────────────┘   │
│                                                                      │
│           BusinessBaseService.getModelBysId(map)                     │
│              │                                                       │
│              ├── 1. formData                                         │
│              │     └── gdsconfigformmaster (filtered by              │
│              │           sParentId = sModelsId; gdsmodule itself     │
│              │           is *not* SELECT-ed, only referenced by id)  │
│              │         + LEFT JOIN gdsconfigformpersonalize          │
│              │           (per-tenant overlay)                        │
│              │         + per-master gdsconfigformslave               │
│              │         + per-master gdsconfigformcustomslave         │
│              │           (per-tenant overlay)                        │
│              ├── 2. gdsformconst (filtered by sParentId only;        │
│              │       NOT tenant-scoped; sLanguage selects which      │
│              │       label column to return)                         │
│              ├── 3. sysjurisdiction (per-user/group grants joined    │
│              │       to sftlogininfojurisdictiongroup +              │
│              │       sisjurisdictionclassify; skipped for ADMIN.     │
│              │       Returned under map key `gdsjurisdiction` —      │
│              │       misleading name, the gdsjurisdiction table is   │
│              │       the builder-side action catalogue, not what's   │
│              │       read here)                                      │
│              ├── 4. sysbillnosettings (per-tenant, per-form)         │
│              └── 5. sysreport (per-tenant, per-form)                 │
└──────────────────────────────────────────────────────────────────────┘
              │
              │  Returns one composite map:
              │     { formData, gdsformconst, gdsjurisdiction,
              │       billnosetting, report }
              ▼
┌──────────────────────────────────────────────────────────────────────┐
│  Browser SPA                                                         │
│    Renders the form using formData; populates dropdowns from         │
│    gdsformconst and per-control SQL fetched separately               │
│                                                                      │
│    Calls /business/getBusinessDataByFormcustomId/{formId}            │
│      with sModelsId={moduleId} to load the actual rows               │
└──────────────────────────────────────────────────────────────────────┘
              │
              ▼
┌──────────────────────────────────────────────────────────────────────┐
│  xlyEntry — BusinessBaseController.getBusinessDataByFormcustomId()   │
│                                                                      │
│    Composes parameterised SQL from formMaster.sSqlStr / sWhere /     │
│    sOrder, with sBrandsId / sSubsidiaryId injected;                  │
│    runs against the form's backing object (table | view | proc).     │
└──────────────────────────────────────────────────────────────────────┘
              │
              ▼
                          User sees the grid

The same flow as a sequence

The ASCII above shows the order of operations; the sequence diagram below shows who calls whom, which is what matters when you're tracing a real request through the runtime.

sequenceDiagram
    autonumber
    participant SPA as Browser SPA
    participant CTRL as BusinessBaseController
    participant SVC as BusinessBaseServiceImpl
    participant FORMS as BusinessGdsconfigformsServiceImpl
    participant DB as MySQL
    participant REDIS as Redis (RedisCacheManager)

    SPA->>CTRL: GET /business/getModelBysId/{sModelsId}<br/>?sModelsId=...&Authorization=<bearer>
    Note over CTRL: AuthorizationInterceptor.preHandle<br/>resolves UserInfo from Redis<br/>RequestAddParamUtil.addParams (16 keys)

    CTRL->>SVC: getModelBysId(map)
    SVC->>FORMS: getModelConfigByModleId<br/>(form-master + slaves + overlays)
    REDIS-->>FORMS: cache hit?
    FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize ⋈ slave ⋈ customslave
    DB-->>FORMS: rows
    FORMS-->>SVC: formData

    SVC->>FORMS: getFormconstData (form-id only, NOT tenant-scoped)
    FORMS->>DB: SELECT ... gdsformconst WHERE sParentId=...
    DB-->>FORMS: rows
    FORMS-->>SVC: gdsformconst

    alt sUserType != ADMIN
      SVC->>FORMS: getJurisdictionData (per-user grants)
      FORMS->>DB: SELECT ... sysjurisdiction ⋈ sftlogininfojurisdictiongroup
      DB-->>FORMS: rows
      FORMS-->>SVC: gdsjurisdiction (map-key; source table is sysjurisdiction)
    else ADMIN
      Note over SVC: skip jurisdiction load
    end

    SVC->>FORMS: getBillnosettingData
    FORMS->>DB: SELECT ... sysbillnosettings WHERE sFormId=... AND tenant
    DB-->>FORMS: row
    FORMS-->>SVC: billnosetting

    SVC->>DB: SELECT ... sysreport WHERE sFormId=... AND tenant
    DB-->>SVC: report rows

    SVC-->>CTRL: composite Map (5 keys)
    CTRL-->>SPA: AjaxResult{code:1, dataset:{...}}

    SPA->>CTRL: POST /business/getBusinessDataByFormcustomId/{formId}<br/>?sModelsId=...
    Note over CTRL,SVC: same RequestAddParamUtil pass<br/>then per-form sSqlStr / sWhere / sOrder
    CTRL->>DB: parameterised SELECT against the form's backing table/view/proc
    DB-->>CTRL: rows
    CTRL-->>SPA: dataset

The two HTTP round-trips are visible at lines 1 and 22 in the diagram. Everything between is server-side work the SPA never sees.

The five-key composite

getModelBysId returns one Java Map with these keys, in this order:

Key Source Used by the SPA for
formData gdsconfigformmaster (filtered by sParentId = sModelsId) ⋈ gdsconfigformpersonalize (per-tenant overlay); per master row, gdsconfigformslave + gdsconfigformcustomslave overlays. gdsmodule is referenced only by id. The form layout itself — every field, control, label, validation rule
gdsformconst gdsformconst rows filtered by sParentId only — NOT tenant-scoped; sLanguage selects which label column to return Form-level constants — labels, defaults, dropdown text
gdsjurisdiction sysjurisdiction rows for the user (or for the user's group via sftlogininfojurisdictiongroupsisjurisdictionclassify); skipped for ADMIN. The map-key name gdsjurisdiction is misleading — that table is the builder-side action catalogue, not what's read here. Per-button and per-data permissions
billnosetting sysbillnosettings row for this module (per-tenant) Document-numbering rules (work-order numbers, quotation numbers)
report sysreport rows linked to this form (per-tenant) Print templates (Excel via jxls, PDF via iText)

What's not in this lifecycle

A few things readers expect to find here but don't:

  • The "save" path. Save is its own endpoint (POST /business/addUpdateDelBusinessData), bundling add+update+delete in one request. See Slice 1.
  • The "open existing row for edit" fetch. Same endpoint as the grid load (getBusinessDataByFormcustomId) but with the row's sId in the body — the handler branches on whether the body asks for one row or many.
  • Workflow steps. When a module has an active approval workflow (bCheck = 1, populated sVersionFlowId, deployed Activiti process), additional steps interleave. None of those tables are populated in this dev DB; see Slice 7 (deferred).
  • Cache invalidation. When BACK changes a metadata row, a JMS message invalidates cached copies on every running node — ConsumerChangeGdsModuleThread in xlyErpJmsConsumer. Outside the request flow but adjacent to it.

Variations covered by other slices

  • Slice 1 — the canonical instance, with observed network traffic.
  • Slice 2 — how the multi-tenant filter threads through every step.
  • Slice 3 — view-backed instead of table-backed.
  • Slice 4 — the gdsconfigformcustomslave merge step.
  • Slice 5 — when a stored procedure body has been replaced for a specific tenant.

Read this page once and keep it open

If a future chapter uses a term you don't recognise, it almost certainly refers to one of the boxes in the diagram above. Come back here.