# Slice 3 — a module backed by a view (a read-only report) Slice 1 traced a CRUD module — a form that reads and writes one table. Slice 3 takes the next step: a module that **reads from a database view**, joining several base tables, and presents the joined result as a sortable, filterable grid. No save, no delete — read-only. This is the foundation of every "list view" in xly: order summaries, daily production reports, stock snapshots, KPI dashboards. The framework treats view-backed modules through the *same* metadata pipeline as table-backed modules, with a single difference: `gdsconfigformmaster.sType = 'view'` suppresses the write operations. ## What we're documenting | | | |---|---| | **Module** | `工单工序明细` (Work-Order Process Details) | | **URL fragment** | `/indexPage/commonList` *(a generic "list-only" page template)* | | **Module `sId`** | `19211681019715708462888040` | | **Backing object** | [`viw_mftworkorderprocess`](../auto-catalog/views/viw_mftworkorderprocess.md) — a database view, **not** a base table | | **Write procs** | None (read-only by design) | | **Print template attached** | None — pure grid | The view it reads from joins the work-order master/slave family (`mftworkordermaster`, `mftworkorderslave`, …) into a flat per-row representation. *That* the view exists is itself part of xly's report-rendering convention: instead of rebuilding the join in every read query, xly authors a database-side `CREATE VIEW` and points one metadata row at it. ## Why this module It's the simplest member of the most common operational pattern in xly: **view-backed list pages**. View-backed forms (`sType = 'view'`) are second only to table-backed forms in the framework's catalog — every operational report and dashboard is a variant of this pattern. Once you've internalised this slice, the rest follow. It's also a counter-example to two things Slice 1 might have made you believe: 1. That every module reads from a single table — view-backed modules read joins. 2. That every URL is a unique pinyin path. `/indexPage/commonList` is a **shared template URL**: many different view-backed modules answer at `/indexPage/commonList`. The URL doesn't identify the module — the `sModelsId` parameter on the API calls does. ## The trace ### 1. Page shell Same as Slice 1: navigating to `/indexPage/commonList` lands the SPA shell; the SPA then resolves which module to load via its sidebar / menu state. The URL is *display state*, not a route driver. ### 2. Metadata fetch ``` GET /xlyEntry/business/getModelBysId/19211681019715708462888040?sModelsId=19211681019715708462888040 ``` Same handler as Slice 1 (`BusinessBaseController.java:63-69`). Same five-key composite response: `formData`, `gdsformconst`, `gdsjurisdiction`, `billnosetting`, `report`. The `report` key is **empty** here — this module has no print template attached (verified: `SELECT COUNT(*) FROM sysreport WHERE sFormId IN (forms-of-this-module)` returned 0). The crucial difference from Slice 1 is in `formData`. The form-master row has: ``` sType = 'view' sTbName = 'viw_mftworkorderprocess' sSqlStr = (typically a SELECT … FROM viw_mftworkorderprocess … skeleton) sWhere/sOrder = (default predicates) ``` The `sType = 'view'` bit is the framework's only signal that this module is read-only. The metadata structure is otherwise identical to a CRUD module's. ### 3. Grid data fetch ``` POST /xlyEntry/business/getBusinessDataByFormcustomId/{formId}?sModelsId={moduleId} ``` The same endpoint as Slice 1. The handler doesn't care whether the backing object is a table or a view — it just substitutes `sTbName` into the parameterised SQL composed from `sSqlStr` + `sWhere` + `sOrder`. The view does the heavy lifting of the join; the framework adds tenant filters and pagination. > **Why a view and not a runtime JOIN?** xly authors the join in SQL DDL > (`CREATE VIEW`) so the framework's runtime SQL stays a flat > `SELECT … FROM WHERE …`. This makes it easy to plug > different "shape" sources (table, view, proc) into the same form > machinery without the runtime caring which it is. The trade is a > proliferation of named `viw_*` views in the schema. ### 4. The view itself `viw_mftworkorderprocess` joins the work-order master with the per-process slave detail. Looking at the [auto-catalog page for this view](../auto-catalog/views/viw_mftworkorderprocess.md) gives you the full `CREATE VIEW` definition. Naming convention: views in xly are prefixed `viw_` or `Viw_` (case inconsistent across the schema). Some join-views are very wide (40+ columns is common). They almost always carry `sBrandsId` / `sSubsidiaryId` from their primary base table so the framework's universal tenant filter still applies — Slice 2's tenant boundary extends through views unmodified. ### 5. Saving — there is none For `sType = 'view'` the framework does not offer Add/Update/Delete buttons. The MyBatis side of `addUpdateDelBusinessData` would technically attempt to write to a view if asked, but the framework doesn't render the buttons for it. (A maintainer wanting to make a particular view *updatable* would either point the form at a base table and use a custom save proc, or rely on MySQL's automatically-updatable-view behaviour — this module does neither.) ### 6. Printable reports (when present) Some view-backed modules *do* have print templates — Excel-via-jxls and PDF-via-iText. The mechanism is separate from the grid: - `getModelBysId` returns the `report` array, populated from `sysreport` rows linked to the form via `sFormId`. - The frontend's "打印" / "导出" buttons hit `xlyEntry/src/main/java/com/xly/web/report/` — `PrintReportController` is the live class. (`PrintReportControllerOld.java` exists in the same directory but its class body is fully commented out, dead source.) The controller loads a jxls / iText template, runs the same view-backed query with a "fetch all rows" wrapper, and streams a binary file back. - This module (`工单工序明细`) has no template attached, so we don't exercise the print path here. **A future revision of this slice should pick a module that *does* — `print template` is a chapter of its own.** ## Concepts this slice introduces (or sharpens) - *View-backed modules* (new concept page): `gdsconfigformmaster.sType = 'view'` makes a module read-only; the rest of the metadata flow is identical. - *The "shared template URL" pattern* — `/indexPage/commonList`, `/indexPage/commonBill`, `/indexPage/commonClassify`, `/indexPage/commonNewBill` are each used by hundreds of modules. The URL is a *page-shape* selector; module identity comes from the `sModelsId` query param, not the path. - *Report templates* (preview only — full coverage in a future slice pass): `sysreport` linked via `sFormId`, jxls/iText templates served by `PrintReportController`. ## Reference entries this slice exercises - [Builder: how to define a virtual table](../reference/builder/define-vtable.md) — for view-backed modules, the form's `sType` and `sTbName` are the declaration. - [Maintainer: the runtime](../reference/maintainer/runtime.md) — same `BusinessBaseController` path as Slice 1; the `sType` branch is the one new bit to document. - New page: *Views and reports* (under Maintainer) — the `viw_` / `Viw_` naming convention, the rule that views must carry tenant columns through to remain tenant-safe, the print-template flow. ## Open verification items 1. **A view-backed module *with* a print template** — pick one and trace the jxls export end-to-end. Likely candidate: any monthly / yearly summary report (`viw_corebusinessreport`, `viw_accordercostanalysisnew`, …). 2. **The `sType = 'proc'` variant** — a sizable minority of forms are backed by stored procedures rather than tables/views. Slice 4 or a variant of this slice should cover that mode: how a proc-backed form returns its result-set, and how parameters flow. 3. **Tenant safety in views.** We claimed views "almost always" carry `sBrandsId` / `sSubsidiaryId`. Worth a script that audits which views *don't* — those are potential cross-tenant leak vectors.