# 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 <one-name> 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.

> **Future-work backlog.** A revision of this slice that picks a
> module *with* an attached print template would let us trace the
> jxls export end-to-end. Blocked on dev-DB state today (no
> view-backed form has a `sysreport` row attached — see the Open
> verification items below).

## 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

> **Item 1 — Deferred (needs populated dev DB).** As of the last audit
> the dev DB has zero view-backed forms with a `sysreport` row attached:
> `SELECT … FROM gdsconfigformmaster m INNER JOIN sysreport r ON
> r.sFormId = m.sId WHERE m.sType='view'` returns 0 rows. The item
> remains a real verification gap that requires a tenant deployment
> whose `sysreport` rows include at least one view-backed form.

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 — audit which `viw_*` lack
   `sBrandsId`.**~~ **CLOSED — 19 of 305 (~6.2 %) leak.** Run against
   the live DB:

   ```sql
   SELECT v.TABLE_NAME
   FROM information_schema.views v
   WHERE v.TABLE_SCHEMA = DATABASE()
     AND v.TABLE_NAME LIKE 'viw_%'
     AND v.TABLE_NAME NOT IN (
       SELECT TABLE_NAME FROM information_schema.columns
       WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'sBrandsId'
     );
   ```

   Returns 19 rows in this dev DB — including `viw_purorder_slave_detail`,
   `viw_qlyprocesstest`, the `viw_accproductstoreinvoice*` family, the
   `viw_hmwxjy*` set, etc. Each is a potential cross-tenant leak
   *if* a form points at it without an enclosing tenant predicate in
   `gdsconfigformmaster.sWhere`. Auditing the form layer's predicates
   for these specific views is the next step; the bare-view audit is
   now a one-shot SQL.
