通用存储过程分发
当元数据命名一个存储过程时(例如通过 gdsmodule.sSaveProName、sSaveProNameBefore、sDeleteProName、sCalcProName、sProcName,或 gdsconfigformslave.sButtonParam),框架会按名称分发该过程。sSaveProName 和 sSaveProNameBefore 是钩子点:它们作为保存后 / 保存前阶段叠加在始终会运行的基础新增 / 更新路径(BusinessBaseServiceImpl.addBusinessData / updateBusinessData)之上,由 BusinessBaseServiceImpl.java:1824 与 :1778 的 checkUpdate(...,"sSaveProName") 调用。其他列驱动按需调用:sCalcProName 用于按钮计算,sProcName 用于自定义取数流等。同一套通用分发机制处理这些情况。
处理器是 xlyEntry/com/xly/web/businessweb/ 中的 GenericProcedureCallController。
端点形状
前端向 /procedureCall/doGenericProcedureCall POST:
- 存储过程名(通常从
gdsmodule或gdsconfigformslave.sButtonParam解析)。 - 参数值(前端提供,框架注入租户身份)。
handler 会:
- 从
information_schema.PARAMETERS加载过程签名(参数名和类型)。 - 拼出
CALL <proc>(?, ?, ...),并按位置绑定参数。 - 用
RequestAddParamUtil增补sBrandsId、sSubsidiaryId、sLoginId、sLanguage。 - 通过 MyBatis 执行。
- 返回过程结果集(过程内 SELECT)或 OUT 参数(保存 / 计算过程)。
为什么动态过程分发重要
schema 中有 1,687 个存储过程和约 177 个函数,把每一个硬编码进 Java 不可承受。按名称分发让框架能调用元数据指向的任意过程,而无需代码变更。框架把过程当作黑盒:输入名称和参数,得到结果。
缺点是:运行时无法静态知道哪些过程存在或它们有什么效果。gdsmodule.sSaveProName 中的拼写错误会变成运行时“proc not found”,不是编译错误。
过程遵循的约定
xly 过程共享一套调用约定,以便通用分发可行:
-
固定顺序的公共参数: 通常是
(IN sLoginId, IN sCustomerId, IN sBrId, IN sSuId, ...),跨切面的身份参数总是在前。 -
租户感知: 每个过程接受
sBrId(=sBrandsId)和sSuId(=sSubsidiaryId),并在内部每个谓词中使用它们保证租户安全。 -
通过 OUT 参数输出: 保存 / 计算过程通过
OUT sCode INT, OUT sReturn LONGTEXT返回成功 / 错误;规范脚手架见xlyEntry/src/main/resources/templates/templesql/sSaveProName.sql。 -
内部动态 SQL: 许多过程用
CONCAT+PREPARE+EXECUTE构造 SQL 字符串。这是 xly 对运行时形状变化的逃生口,也是依赖树中存在jSqlParser的原因(框架偶尔会检查这些字符串)。
配套文件:templesql/
xlyEntry/src/main/resources/templates/templesql/ 保存标准过程类型的脚手架,当前树中有 8 个文件:
sSaveProName.sqlsDeleteProName.sqlsProcName.sqlsSaveProNameBefore.sqlsButtonParam.sqlsProTempleByDb.sqlsp_btn_calc.sqlsSqlStr.sql
这些是工程师编写新过程时填充的模板;运行时不会用它们动态生成过程。loader 和占位符语法见 SQL 模板。
实时 schema 中的过程命名模具
实时 DB 的 1687 个存储过程围绕少数命名模具聚集,不只是裸 Sp_* 家族:
| 模具 | 约数 | 角色 |
|---|---|---|
Sp_* |
大多数 | 主导家族,由 sSaveProName / sCalcProName / sProcName 等分发。 |
Sp_*_BeforeSave |
~62 | 保存前钩子,对应 sSaveProNameBefore。 |
Sp_*_AfterSave / Sp_*_SaveReturn
|
~62 / ~54 | 保存后钩子;_SaveReturn 会写回父事务。 |
Sp_*_Calc |
~178 | 按钮计算过程,由按钮流程(sCalcProName / sButtonParam)调用。 |
sp_btn_* |
~65 | 按钮事件子家族,通常是 sp_btn_calc* / sp_btn_validate*(约定使用小写)。 |
PRO_ERPMERGE* |
~11 | 数据迁移 / 合并工具。不由运行时分发,只给工程师直接使用。 |
PRO_*(其他) |
~12 | 其他一次性工具。 |
Get_*、del_*、Cal*、Tj_*
|
少量 | 历史 / 领域特定 helper,不属于通用分发契约。 |
被分发列里如果写错过程名,通常也会期望目标是 Sp_* 形状;其他模具不会通过 sSaveProName / sCalcProName 等解析。非 Sp_* 过程只能通过 mapper XML 或其他过程直接调用。
函数层
schema 还包含 177 个用户自定义函数,命名模具与过程平行:Fun_*(约 150)、Fn_*(约 8)、get_*(约 10)。
这些函数不由 Java 分发。它们从其他存储过程、视图定义和 mapper XML 的 SELECT 语句中被调用。没有 gdsmodule.sFunctionName 之类的元数据列;函数由提到它们的 SQL 自行引入。维护人员排查慢报表时,应在过程和视图里 grep Fun_* / Fn_* / get_* 引用;框架 Java 侧看不到这些函数。
需要关注的失效模式
- 参数顺序不匹配。 通用分发按位置绑定;IN 参数重排的过程会在运行时炸掉。
-
缺少租户过滤。 某过程在内部谓词中忘记
sBrandsId/sSubsidiaryId,就是多租户泄漏。过程不会被框架自动过滤,必须自己处理。 -
长运行过程阻塞请求线程。 长计算应放到独立后台线程或任务服务中(见
xlyErpTask,尽管它当前在settings.gradle中禁用)。