# 如何定义虚拟表 xly 中的*虚拟表*是作为元数据声明的“表”,不是 DDL 创建的真实表。框架之所以知道它,是因为 `gdsconfigtbmaster` 和 `gdsconfigtbslave` 中的行描述了它的形状;实际数据存储在真实物理表中,但框架操作的*抽象*是元数据声明。 这不同于数据库**视图**,视图是 `CREATE VIEW` SQL 对象。两者都可以通过 `gdsconfigformmaster.sType = 'table'` 或 `'view'` 支撑表单。 ## 虚拟表用途 - 让 PM 声明“我需要这种形状的东西”,无需工程师执行 `CREATE TABLE`。 - 定义下游表单可以叠加使用的数据形状。 - 集中管理租户感知的列定义,使多个读取同一形状的表单共享默认值。 虚拟表覆盖 lookup 表、分类树和可配置参数集。随着 PM 增加新形状,目录会自由增长。 ## 配方 ### 1. 虚拟表 master — `gdsconfigtbmaster` 每个虚拟表一行: | 列 | 值 | |---|---| | `sId` | 唯一虚拟表 ID | | `sChinese` / `sEnglish` / `sBig5` | 显示名 | | `sBrandsId` / `sSubsidiaryId` | 租户作用域 | | `sTbName` | 底层物理名称。**实践中它可以指向表、视图或存储过程**;该列有唯一键,但没有更严格约束。运行时把它当成通用 SQL 标识符解析。 | | `sParentId` | 树形分类的父虚拟表;平铺表为空 | | `iOrder` | BACK 列表中的排序 | ### 2. 列 — `gdsconfigtbslave` 每列一行。每行携带列名、类型、默认值、显示标签、校验规则,以及它是否属于主键。 ## `sTbName` 实际指向什么,以及漂移 每个 `gdsconfigtbmaster` 行都有非空 `sTbName`,但该列只是带唯一键的字符串;框架不会强制它解析为基础表。已对实时 dev DB 验证: - `gdsconfigtbmaster` 总计 307 行。 - **296 行(96.4%)解析到 `information_schema.tables` 中真实存在的 `BASE TABLE`**。 - **11 行(3.6%)无法解析为基础表**,而它们的分布本身很有信息量: | 未解析的 `sTbName` 实际指向 | 数量 | 示例 | |---|---:|---| | 视图(`viw_*`),不是表 | 4 | `viw_mftproductionreport`、`viw_mftproductionreportEmployee1` | | 存储过程(`Sp_*`) | 3 | `Sp_Cashier_BankJournal`、`Sp_Cashier_SumJournal`、`Sp_Sales_NotDeliverGoodNotifyList` | | 大小写折叠后存在的真实表,或已重命名 / 删除对象 | 4 | `QlyProcessTestResult`(大小写漂移)等 | 所以 `sTbName` **不严格等于“物理表名”**;它是运行时会替换进读取查询里的通用 SQL 标识符,也可能指向视图或可调用过程。早先把它写成“底层物理表名”的说法过窄。 可用于暴露漂移的审计 SQL: ```sql SELECT sId, sChinese, sTbName FROM gdsconfigtbmaster WHERE sTbName NOT IN ( SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE() ); ``` ## 何时选择虚拟表、视图或真实表 | 需求 | 选择 | |---|---| | PM 声明的新形状,并由真实(可能已有)表支撑 | 虚拟表 | | 跨已有表的只读 join | 数据库视图 | | 真正需要新存储且没有现有表可用的新形状 | 真实 `CREATE TABLE`(工程任务) | 虚拟表通道是框架对数据驱动形状的“类型系统”;物理 schema 才是真正存储行的地方。两者有意解耦。 ## 示例:`包装方式` lookup dev DB 中的一条代表性真实记录: **Master**(`gdsconfigtbmaster`): ```text sId = 192116810113315231587698560 sChinese = 包装方式 (Packing method) sTbName = SisPacking sParentId = (root) ``` **Slave 列**(`gdsconfigtbslave`,该 `sParentId` 下 10 行)声明的是*逻辑*形状:名称、显示标签、校验。*物理*形状位于真实的 `SisPacking` 表中: | Slave 行 | `SisPacking` 上的物理列 | |---|---| | `iIncrement`(自增列) | `iIncrement int auto_increment PK` | | `sId`(标准ID) | `sId varchar(100) UNIQUE` | | `sBrandsId`(加工商Id) | `sBrandsId varchar(100)` | | `sSubsidiaryId`(子公司Id) | `sSubsidiaryId varchar(100)` | | `tCreateDate`(制单日期) | `tCreateDate datetime DEFAULT CURRENT_TIMESTAMP` | | `sMakePerson`(制单人) | `sMakePerson varchar(255)` | | `iOrder`(排序号) | `iOrder int DEFAULT 0` | | `sName`(名称) | `sName varchar(255)` | | `sNo`(编号) | `sNo varchar(255)` | | `bInvalid`(作废) | `bInvalid bit(1) DEFAULT b'0'` | `gdsconfigtbslave` 中的 10 行与物理 `SisPacking` 表上的 10 列逐一对应。PM 随后可以让某条 `gdsconfigformmaster` 指向 `sTbName='SisPacking'`,form-slave 行再按名称引用同一批列。运行时把这两层粘合起来,路径与切片 1 中的元数据驱动读取相同。 本页之前把“补一个 worked example”列为 TODO;这里就是补上的示例。