# 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. ```mermaid 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}
?sModelsId=...&Authorization= Note over CTRL: AuthorizationInterceptor.preHandle
resolves UserInfo from Redis
RequestAddParamUtil.addParams (16 keys) CTRL->>SVC: getModelBysId(map) Note over SVC: getModelConfigByModleId (inherited from BaseServiceImpl)
orchestrates the per-master form-master + slave loads SVC->>FORMS: getFormmasterData / getGdsconfigformslaveShow
(form-master + slaves + overlays) REDIS-->>FORMS: cache hit? FORMS->>DB: SELECT ... gdsconfigformmaster ⋈ personalize; per master row, gdsconfigformslave + gdsconfigformcustomslave 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}
?sModelsId=... Note over CTRL,SVC: same RequestAddParamUtil pass
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 `sftlogininfojurisdictiongroup` ⋈ `sisjurisdictionclassify`); 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](../slices/01-hello-world.md#4-user-edits-a-row-clicks-save). - **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`, `gdsmoduleflow` configured, deployed Activiti process, and `ConstantUtils.bCheckflowCheck = true`), additional steps interleave. None of those tables are populated in this dev DB; see [Slice 7 (deferred)](../slices/07-workflow.md). - **Cache invalidation.** When BACK changes a metadata row, the save path synchronously calls `BusinessCleanRedisData` / `CleanRedisServiceImpl`, which evicts Spring cache regions from shared Redis. The JMS `ConsumerChangeGdsModuleThread` path is a separate base-data merge channel, not cache invalidation. ## Variations covered by other slices - [Slice 1](../slices/01-hello-world.md) — the canonical instance, with observed network traffic. - [Slice 2](../slices/02-multi-tenancy.md) — how the multi-tenant filter threads through every step. - [Slice 3](../slices/03-report.md) — view-backed instead of table-backed. - [Slice 4](../slices/04-custom-field.md) — the `gdsconfigformcustomslave` merge step. - [Slice 5](../slices/05-customer-sql-override.md) — 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.