

「定义是唯一真相」听起来像是有一份文件装着全部真相。实际打开一个项目的定义目录,看到的是十三种定义类型:有的是根目录下的单一文件,有的一支程序一份、散在子文件夹里。
这一篇是地图。后面每谈一个机制都会用到其中几份,先在这里认识,就不必每次停下来解释。第四节末尾有一张十三份的速查表,标出每一份由哪一天展开,读到不确定的名字可以翻回这里。
本篇说明:
- 启动三件组:
SystemSettings/DatabaseSettings/DbCategorySettings - 一张表单的三份:
FormSchema/TableSchema/FormLayout - 绑定与导航:
ProgramSettings/MenuSettings/PluginSettings - 所有表单共用的四份:
LanguageResource/PermissionModels/CurrencySettings/UnitSettings - 十三份共通的四件事:存储、缓存、不可变更、找不到时的处理
表格第一列可点进源代码。
一、启动三件组:这个部署跑在什么上面
| 定义 | 存法 | 管什么 |
|---|---|---|
SystemSettings |
单文件 | 主密钥从哪里取、payload 怎么打包与加密、调试模式 |
DatabaseSettings |
单文件 | 这个部署有哪些物理数据库、各自怎么连上去 |
DbCategorySettings |
单文件 | 哪一张数据表属于哪一类、该类由哪一个数据库承载 |
这三份是系统起得来的前提,而且读取顺序固定:
SystemSettings(指名主密钥在哪里)
→ 主密钥解出保护设置用的密钥
→ 解开 DatabaseSettings 里的连接密码
→ DbCategorySettings 写的数据库代号才对得到东西
顺序不是惯例,是依赖性算出来的唯一解。倒过来走的路根本不存在:在数据库连得上之前,没有任何机会先去数据库查一把密钥。
链条的第一份必须在什么都还没起来时就读得懂,所以它自己不能加密。主密钥因此不放在 SystemSettings 里面,那份文件只记「密钥在哪里」:
<MasterKeySource>
<Type>Environment</Type>
<Value>BEE_MASTER_KEY</Value>
</MasterKeySource>
框架也据此把启动时那一次读取独立出来:不通过任何服务、不经过缓存,直接读文件,因为在那个时间点其他东西都还不存在。
数据库的事为什么是两份
两者回答不同的问题:
DatabaseSettings |
DbCategorySettings |
|
|---|---|---|
| 回答什么 | 这个部署有哪些数据库、怎么连 | 哪一类数据落在哪一个数据库 |
| 性质 | 部署环境的事实 | 应用结构的事实 |
| 什么时候改 | 搬机房、换数据库服务器、测试环境改指向 | 添加一张表、把某批数据换一个承载的数据库 |
| 敏感度 | 装连接密码,要加密、要管访问 | 没有秘密,开发者天天改 |
分家的好处是两边各改各的,顺带把敏感的与日常的隔开。
数据实际上会落在几个数据库、跟数据表命名前缀是什么关系,Day 11 会专门谈。
定义层里有数据库设置,却不认得数据库
一份记着连接字符串的文件归在定义层,看起来像分层破了。没有破,因为它放的是描述,不是连接。
定义层这个包的引用清单极短,它不知道有数据库、不知道有 HTTP、不知道有 UI,连接字符串在它眼里只是一段字符串加一个代号。真正开连接的是数据访问层:定义层只声明一个「拿得到 DatabaseSettings」的接口,由它去取用。
二、一张表单的三份定义
| 定义 | 存法 | 管什么 |
|---|---|---|
FormSchema |
一支程序一份 | 定义中枢:字段、类型、跨表关联、主表与明细、计算列与验证规则 |
TableSchema |
一张表一份 | 物理数据表的字段、类型、长度、允不允许 NULL、索引 |
FormLayout |
一张布局一份 | 这些字段在界面上怎么排 |
Day 1 那个「订单加预计到货日」要改的三处,就是这三份。
FormSchema 是中枢,另外两份各自对着一个方向:一个对数据库,一个对界面。
FormSchema:声明有哪些字段、什么类型、哪些指向别的表单、主表配了哪几张明细、哪些值是算出来的、保存前检查什么。框架拿它组 CRUD SQL、决定数据跨层传递的形状。TableSchema:关心的是物理存储,不是业务语义,所以长度、索引、精度可以由熟悉数据库的人单独调整。FormLayout:三份里唯一只管界面的。怎么排是设计阶段的决定,写成一份布局定义文件,运行时照它排。
三份的关系也决定改动顺序:FormSchema 描述表单有哪些字段,改动从它开始;TableSchema 跟着补物理字段,要显示才进 FormLayout。
三、绑定与导航:这支程序由谁接手、出现在哪里
| 定义 | 存法 | 管什么 |
|---|---|---|
ProgramSettings |
单文件 | ProgId 对到哪个 Business Object 与哪个 Repository,留空就走框架默认 |
MenuSettings |
单文件 | 导航树的分组、排序、标题与可见性,每个项目指向一支程序 |
PluginSettings |
单文件 | 每支程序在保存与删除的时点要跑哪几个 plugin,依声明顺序运行 |
ProgramSettings是一张注册表,每支程序一行,决定这支程序的请求由谁处理。两个绑定字段留空就走框架默认的 CRUD 流程;填了类名称,就由那个类接手。Day 3 说案例八张表单只有订单要写程序,看的就是这张表。MenuSettings跟注册表分家,是因为读它的不是同一端:注册表由服务端读,装的是代码里的类名称;菜单由前端读,装的是排序、标题与可见性。而且同一支程序本来就可能出现在菜单的多个位置,所以节点有自己的标识码,不用 ProgId 当键。PluginSettings记的是每支程序在保存与删除前后要跑的那几个 plugin 与顺序。跟继承改写的差别 Day 3 提过(继承是取代、只能一个;plugin 是叠加、可以多个),细节留给 Day 21。
四、每一张表单都会用到的那几份
| 定义 | 存法 | 管什么 |
|---|---|---|
LanguageResource |
每种语言一组,再依表单或分类切分文件 | 本地化的标题与选项文字 |
PermissionModels |
单文件 | 有哪些权限模型、各自管哪些动作、数据范围怎么界定 |
CurrencySettings |
单文件 | 各币种的小数位与最小流通面额 |
UnitSettings |
单文件 | 各计量单位的显示小数位 |
这四份不属于任何一张表单,但每一张表单都会查到它们。
LanguageResource是逐条叠上去的。FormSchema里本来就写了字段标题,语言资源提供各语言版本,对得上就换掉,对不上就用原本那个。不需要多语的项目完全不必碰,加了也不会逼你把每一条都翻完。PermissionModels登记的是「有哪些权限可以谈」。哪些动作需要授权、数据范围怎么界定,这些是模型;至于谁被授予了什么,那是数据不是定义,不在这份文件里(Day 24 展开)。- 币种与计量单位是系统层的:某个币种有几位小数、某个单位显示几位,全系统一致。而「这家公司准用哪几种币种、现金要舍入到多少」是公司层设置,跟着公司数据从数据库来,不是定义文件。两层各有位置,Day 26 会用到。
十三份速查
前面四节是依用途分组,这里把十三份摊在一起,顺序不变:
| 定义 | 管什么 | 哪一天展开 |
|---|---|---|
SystemSettings |
主密钥来源、payload 选项、调试模式 | 本篇 |
DatabaseSettings |
这个部署有哪些数据库、各自怎么连 | 本篇 |
DbCategorySettings |
哪一类数据落在哪一个数据库 | 本篇 |
FormSchema |
定义中枢:字段、类型、关联、主表与明细、规则与计算列 | Day 5 |
TableSchema |
物理数据表:字段、长度、允不允许 NULL、索引 | Day 6 |
FormLayout |
这张表单在界面上怎么排 | Day 7 |
ProgramSettings |
ProgId 绑哪一个 Business Object 与 Repository,只给服务端 | Day 15 |
MenuSettings |
导航菜单的分组、排序、标题与可见性 | Day 15 |
PluginSettings |
每个 ProgId 挂哪些 plugin、依声明顺序运行 | Day 21 |
LanguageResource |
每种语言一组的标题与选项文字 | Day 20 |
PermissionModels |
有哪些权限可以谈:动作与数据范围策略 | Day 24 |
CurrencySettings |
各币种的小数位与最小流通面额 | Day 26 |
UnitSettings |
各计量单位的显示小数位 | Day 26 |
这些名字在后面二十几天会反复出现,而且一律用原文。中文说法讲不清楚是哪一份:
「分类文件」可能是 DbCategorySettings,也可能是 PermissionModels;
「配置文件」十三份里有八份都叫这个名字。
五、十三份共通的四件事
都是 XML,存在哪里是另一回事
定义内容是 XML,放在哪里可以换:磁盘上的文件,或数据库里的一张表(一行一份定义,字段里放那段 XML)。框架通过同一个接口去读,换一种放法,上面每一层都不必知道。
运行的时候用到的不是那段 XML。框架把它读进来、还原成一个定义对象,之后要查什么都是问这个对象。XML 只是它躺在磁盘或数据库时的样子。
读进来之后就留在内存里
缓存里放的就是这些还原出来的对象,一份定义一格,淘汰看的是使用频率:被读到就重新计时,一段时间没人碰才丢掉。
- 天天在用的那几张表单,定义几乎一直在内存里
- 一年开两次的那张,用完没多久就退掉
- 服务器不必为了没人开的表单一直扛着它们的定义
这是定义切成一份一份的回报之一:缓存的单位就是读取的单位,内存用量跟得上实际使用量,而不是跟着定义总量走。
留着就有一件事要交代:源头被改掉了怎么办。每一份缓存各自盯着自己的来源,文件就盯修改时间,数据库就走另一套通知机制;来源一变,下一次要用的时候重读一份新的(Day 12 展开)。
拿出来的定义不可以改
从框架取得一份定义,拿到的是共用的那一份,不是副本。同一台服务器上,所有用户拿到的是同一个对象。
代价是那份对象从此不能碰:
- 要一份只给这个用户看的视图(例如把标题换成他的语言)→ 先拷贝再改
- 要把变更写回去 → 走框架的保存方法,它写文件并让缓存失效
被禁掉的只有「直接在共用那一份上动手」这一种写法,而它本来就是错的,改到的是所有人的东西。
找不到的时候,分两类
框架内嵌了一组默认定义,但它在运行时完全不参与。框架只读你指定的定义目录,内嵌那组是开新项目时的起点,用工具拷贝进去,一次性动作。
不做回退是刻意的。如果 FormSchema 找不到就自动改用内嵌那份,系统会照样跑起来,然后用着一份你根本没放进去的定义。「你忘了部署那个文件」会被改写成「系统行为跟你想的不一样」,而后者要花的追查时间高一个量级。
但「找不到」不是只有一种意思,所以十三种逐一分成两类:
| 类 | 行为 | 哪几份 |
|---|---|---|
| 缺了就是设置错了 | 直接报错,信息指名哪一个文件 | FormSchema、TableSchema、ProgramSettings、PermissionModels,以及启动三件组 |
| 缺了是合法答案 | 回一个「没有」,由调用端接手 | FormLayout、LanguageResource(用结构里的标题)、CurrencySettings / UnitSettings(用默认位数)、MenuSettings、PluginSettings |
这个分类写在程序里,而且必须由框架决定,不能让每个应用各自解读。否则同一个疏漏在不同模块会有不同表现,一个报错、一个安静地用了默认值,那比两个都报错难查得多。
六、案例:拷贝过来,再往上加
Northwind 用到框架提供的部门与员工两张表单,做法就是上一节那个一次性动作:把默认定义拷贝进应用自己的定义目录。
拷贝之后,应用自己在员工上加了两个字段,这两行就在案例的 Employee.FormSchema.xml 里:
<FormField FieldName="title" Caption="Title" DbType="String" />
<FormField FieldName="hire_date" Caption="Hire Date" DbType="Date" />
加完之后那份定义就完全归应用所有,想怎么改就怎么改。反过来,框架日后在默认定义上添加的字段不会自己跑进来,也不会有任何提醒;要不要跟上,自己去改那份文件。
框架的那几个系统字段则不能拿掉,权限与组织的机制认的就是它们。
这是「定义是数据」在维护面上的具体样子:一份数据拷贝出去自己维护,跟一支程序分支出去自己维护,要付的东西不一样。前者不会有合并冲突,也不必逐版把主干的修正搬回来,代价是新东西不会自动跟上。
十三份,一个真相
十三份的形状大致是:三份决定系统跑在什么上面,三份描述一张表单,三份决定谁来运行与从哪里进来,四份是所有表单都会查的共用数据。
这十三份不是十三个配置文件,是同一个真相的十三个面向。它们彼此指来指去(DbCategorySettings 指向 DatabaseSettings、FormSchema 指向别的 FormSchema、ProgramSettings 与 MenuSettings 各自指向同一支程序),却不打架,靠的是每一份都只回答自己该回答的那个问题。
明天谈 FormSchema 怎么变成 SQL。
本文为 2026 iThome 铁人赛参赛文章,原文为繁体中文。
系列完整目录:ERP 架构师笔记:定义驱动的框架设计
文章摘自:https://www.cnblogs.com/jeff-tsai/p/23490258/day-04-definition-files
