从第一性原理看 PowerBuilder一个 90 年代企业级开发工具为什么这样设计不教语法。讲设计决策。以及 30 年后这些问题换了个壳还在。文章目录从第一性原理看 PowerBuilder一个 90 年代企业级开发工具为什么这样设计引子问题一数据的查询、展示、编辑、校验、存储能不能一个东西搞定第一性需求PB 的回答DataWindow今天对应什么问题二数据库连接和事务能不能统一管理第一性需求PB 的回答SQLCA 事务对象今天对应什么问题三窗口之间怎么传数据第一性需求PB 的回答Message 对象今天对应什么问题四代码怎么组织才不会乱第一性需求PB 的回答命名约定今天对应什么问题五怎么调外部系统第一性需求PB 的回答外部函数声明今天对应什么问题六批量数据怎么处理第一性需求PB 的回答游标 存储过程今天对应什么总结30 年前和今天问题没变引子PowerBuilderPB是一个 90 年代的企业级快速应用开发工具。它的鼎盛时期早已过去但大量医院信息系统、社保系统、银行后台至今还在跑 PB 写的客户端。如果你觉得这是一个过时技术、不值得了解——那你可能忽略了一件事PB 在 30 年前解决的问题跟今天的前端框架、ORM、事务管理器解决的是同一批问题。只不过当时叫 DataWindow今天叫 React Redux Form当时叫 SQLCA今天叫 Spring TransactionManager。从第一性原理出发我们不看语法看为什么。问题一数据的查询、展示、编辑、校验、存储能不能一个东西搞定第一性需求企业级应用 80% 的页面是表格查一批数据、展示出来、用户编辑几行、校验格式、提交保存。在 Web 时代你要写 API 接口、写前端组件、写表单校验、写 ORM 映射——四件事四套代码。PB 的回答DataWindowPB 的核心创新是 DataWindow数据窗口。一个控件同时搞定查询配置 SQL 或存储过程DataWindow 自动执行并填充数据展示表格、表单、标签页、图表——一套数据多种展示格式编辑用户直接在 DataWindow 里改数据控件自动跟踪哪些行被修改了校验列级别设置校验规则输入非法值自动拦截存储一行代码dw_1.update()自动生成 INSERT/UPDATE/DELETE 语句提交数据库// 查询 dw_1.SetTransObject(SQLCA) dw_1.Retrieve() // 保存——只这一行 dw_1.Update()没有 API 层没有 JSON 序列化没有前端状态管理。一个控件数据从数据库到屏幕再到数据库全链路打通。今天对应什么DataWindow 做的事今天的方案数据查询 填充ORMMyBatis / Hibernate表格/表单展示React Table / Ant Design Form编辑状态跟踪Redux Form / Formik 的 dirty tracking列级校验Yup / Zod schema validation自动生成 SQLTypeORM 的save()/ MyBatis-Plus 的saveOrUpdate()PB 用一个控件干了今天需要四层架构配合才能干的事。代价是耦合度高——UI 和数据库绑死了。好处是开发速度极快——一个下午能搭一个完整的增删改查页面。问题二数据库连接和事务能不能统一管理第一性需求每个业务方法都要连数据库。如果每个方法自己获取连接、自己提交、自己回滚一定会出事忘 commit 的、忘 close 的、异常没 rollback 的。更麻烦的是——一个操作可能涉及多张表、多个 SQL必须要么全成功要么全回滚。PB 的回答SQLCA 事务对象PB 提供一个全局事务对象SQLCASQL Communications Area。应用启动时配置一次连接参数所有数据库操作共享这个连接// 配置只做一次 SQLCA.DBMS MSS Microsoft SQL Server SQLCA.Database twhis SQLCA.ServerName 192.168.70.33 SQLCA.LogId sa SQLCA.AutoCommit False CONNECT; // 使用每个地方都通过 SQLCA INSERT INTO user_tab (name) VALUES (:ls_name); COMMIT;需要多个独立事务创建新的事务对象Transaction gtr_user1 gtr_user1 CREATE Transaction // 配置另一个数据库连接... CONNECT USING gtr_user1;用完销毁DESTROY gtr_user1AutoCommit False意味着框架不会自动提交——开发者显式写COMMIT或ROLLBACK事务边界清晰可控。今天对应什么SQLCA≈ Spring 的DataSourcePlatformTransactionManagerAutoCommit False 手动COMMIT≈Transactional注解声明事务边界CREATE Transaction创建独立事务 ≈ Spring 的TransactionTemplate编程式事务PB 把连接管理和事务控制提到了框架级别。开发者不碰连接只管逻辑。问题三窗口之间怎么传数据第一性需求一个应用有几十个窗口。用户在窗口 A 输入了参数点按钮打开窗口 BB 要用 A 的参数。怎么办全局变量满天飞写文件中转PB 的回答Message 对象PB 提供一个全局Message对象专门用于窗口间传参// 窗口 A打开窗口 B同时传参数 integer age 50 OpenWithParm(w_to_open, age) // 窗口 B关闭时返回数据给 A CloseWithReturn(Parent, sle_signon_id.Text) // 窗口 A接收返回值 String ls_result Message.StringParm不用全局变量不用写临时文件。OpenWithParm打开窗口的同时把参数塞进 Message目标窗口直接从 Message 取。CloseWithReturn关闭窗口的同时把结果塞回 Message调用方直接读。今天对应什么Message对象 ≈ Vue 的 EventBus / React 的 ContextOpenWithParm≈ React Router 的state参数navigate(/detail, { state: { id: 123 } })CloseWithReturn≈ 弹窗组件的onClose(result)回调思路一样不直接耦合两个组件通过一个中间层传递数据。PB 在 90 年代就用了这个模式。问题四代码怎么组织才不会乱第一性需求项目一大窗口、函数、数据窗口、结构体满天飞。命名混乱不知道谁是谁。三个人协作互相覆盖。PB 的回答命名约定PB 用前缀强制区分类型前缀类型说明w_窗口Windoww_main、w_logindw_数据窗口DataWindowdw_user_listddlb_下拉列表ddlb_deptgf_全局函数Global Functiongf_calc_agest_静态文本StaticTextst_titlecb_按钮CommandButtoncb_save这不是框架强制是社区约定。所有 PB 开发者都这么写。拿到一个陌生项目看前缀就知道每个东西是什么。今天对应什么PB 的前缀约定 ≈ Java 的包命名约定com.company.module.service≈ React 的组件命名约定UserList.jsx、useAuth.js≈ 数据库的字段命名约定user_name、create_time本质都是同一件事约定大于配置。不需要文档名字本身就是文档。问题五怎么调外部系统第一性需求企业系统不是孤岛。社保系统要调银行接口、调医保接口、调第三方加密机。这些外部系统不认你的数据库连接只认 DLL 或 Socket。PB 的回答外部函数声明PB 用FUNCTION关键字直接声明 DLL 接口像调本地函数一样调外部系统// 声明 DLL 函数 FUNCTION int BUSINESS_HANDLE(string InputString, ref string OutputString) LIBRARY SiInterface.dll // 调用 String OutputString OutputString Space(4000) // 预分配内存 BUSINESS_HANDLE(, ref OutputString)注意ref关键字——DLL 修改的是引用传递的参数PB 要预分配好内存空间Space(4000)否则会崩溃。这是 C 语言互操作的基本规则PB 直接暴露给了开发者。今天对应什么DLL 调用 ≈ Java 的 JNIJava Native Interfaceref参数 预分配内存 ≈ C 的指针传参更高层的对应微服务时代的 HTTP/REST API 调用、gRPC 调用PB 时代的集成是进程内 DLL 调用今天是跨进程网络调用。形式变了但问题没变你的系统怎么跟别人的系统说话。问题六批量数据怎么处理第一性需求往数据库插一万条数据逐条 INSERT 太慢。或者要从数据库读出几千条数据逐行处理。怎么高效操作PB 的回答游标 存储过程游标——逐行读取查询结果DECLARE one CURSOR FOR SELECT user_name FROM user_tab ORDER BY user_name; OPEN one; FETCH one INTO :ls_username; DO WHILE SQLCA.SQLCode 0 ddlb_user.AddItem(ls_username) FETCH one INTO :ls_username; LOOP CLOSE one;存储过程——把批量操作推给数据库执行DECLARE Emp_proc PROCEDURE FOR GetName emp_name :Emp_name_var, emp_salary :Emp_sal_var; EXECUTE Emp_proc; FETCH Emp_proc INTO :HostVariableList; CLOSE Emp_proc;PB 的游标语法跟嵌入式 SQL 几乎一样——因为 PB 的设计哲学就是把 SQL 当作一等公民不是字符串拼接而是语言的一部分。今天对应什么游标 ≈ JDBC 的ResultSet逐行遍历存储过程 ≈ MyBatis 的{call procedure_name()}批量操作 ≈ MyBatis 的foreach批量 INSERT总结30 年前和今天问题没变30 年前 PB 解决的问题PB 的方案今天的方案数据全链路查/显/编/校/存DataWindowReact Formik ORM连接和事务管理SQLCASpring Transactional组件间通信Message 对象EventBus / Context代码组织前缀命名约定包命名 / 文件命名约定外部系统集成DLL 声明REST API / gRPC批量数据处理游标 存储过程MyBatis foreach / JDBC Batch框架换了语言换了架构换了。但企业级应用的核心问题——数据怎么流、事务怎么管、组件怎么通信、系统怎么集成——从 90 年代到现在一个都没变。PB 的设计者在 30 年前做出的决策跟今天 Spring 团队、React 团队做出的决策背后是同一套第一性原理。理解了这一点学任何新框架都不会从零开始——因为问题你已经见过了只是解法换了个名字。
从第一性原理看PowerBuilder
从第一性原理看 PowerBuilder一个 90 年代企业级开发工具为什么这样设计不教语法。讲设计决策。以及 30 年后这些问题换了个壳还在。文章目录从第一性原理看 PowerBuilder一个 90 年代企业级开发工具为什么这样设计引子问题一数据的查询、展示、编辑、校验、存储能不能一个东西搞定第一性需求PB 的回答DataWindow今天对应什么问题二数据库连接和事务能不能统一管理第一性需求PB 的回答SQLCA 事务对象今天对应什么问题三窗口之间怎么传数据第一性需求PB 的回答Message 对象今天对应什么问题四代码怎么组织才不会乱第一性需求PB 的回答命名约定今天对应什么问题五怎么调外部系统第一性需求PB 的回答外部函数声明今天对应什么问题六批量数据怎么处理第一性需求PB 的回答游标 存储过程今天对应什么总结30 年前和今天问题没变引子PowerBuilderPB是一个 90 年代的企业级快速应用开发工具。它的鼎盛时期早已过去但大量医院信息系统、社保系统、银行后台至今还在跑 PB 写的客户端。如果你觉得这是一个过时技术、不值得了解——那你可能忽略了一件事PB 在 30 年前解决的问题跟今天的前端框架、ORM、事务管理器解决的是同一批问题。只不过当时叫 DataWindow今天叫 React Redux Form当时叫 SQLCA今天叫 Spring TransactionManager。从第一性原理出发我们不看语法看为什么。问题一数据的查询、展示、编辑、校验、存储能不能一个东西搞定第一性需求企业级应用 80% 的页面是表格查一批数据、展示出来、用户编辑几行、校验格式、提交保存。在 Web 时代你要写 API 接口、写前端组件、写表单校验、写 ORM 映射——四件事四套代码。PB 的回答DataWindowPB 的核心创新是 DataWindow数据窗口。一个控件同时搞定查询配置 SQL 或存储过程DataWindow 自动执行并填充数据展示表格、表单、标签页、图表——一套数据多种展示格式编辑用户直接在 DataWindow 里改数据控件自动跟踪哪些行被修改了校验列级别设置校验规则输入非法值自动拦截存储一行代码dw_1.update()自动生成 INSERT/UPDATE/DELETE 语句提交数据库// 查询 dw_1.SetTransObject(SQLCA) dw_1.Retrieve() // 保存——只这一行 dw_1.Update()没有 API 层没有 JSON 序列化没有前端状态管理。一个控件数据从数据库到屏幕再到数据库全链路打通。今天对应什么DataWindow 做的事今天的方案数据查询 填充ORMMyBatis / Hibernate表格/表单展示React Table / Ant Design Form编辑状态跟踪Redux Form / Formik 的 dirty tracking列级校验Yup / Zod schema validation自动生成 SQLTypeORM 的save()/ MyBatis-Plus 的saveOrUpdate()PB 用一个控件干了今天需要四层架构配合才能干的事。代价是耦合度高——UI 和数据库绑死了。好处是开发速度极快——一个下午能搭一个完整的增删改查页面。问题二数据库连接和事务能不能统一管理第一性需求每个业务方法都要连数据库。如果每个方法自己获取连接、自己提交、自己回滚一定会出事忘 commit 的、忘 close 的、异常没 rollback 的。更麻烦的是——一个操作可能涉及多张表、多个 SQL必须要么全成功要么全回滚。PB 的回答SQLCA 事务对象PB 提供一个全局事务对象SQLCASQL Communications Area。应用启动时配置一次连接参数所有数据库操作共享这个连接// 配置只做一次 SQLCA.DBMS MSS Microsoft SQL Server SQLCA.Database twhis SQLCA.ServerName 192.168.70.33 SQLCA.LogId sa SQLCA.AutoCommit False CONNECT; // 使用每个地方都通过 SQLCA INSERT INTO user_tab (name) VALUES (:ls_name); COMMIT;需要多个独立事务创建新的事务对象Transaction gtr_user1 gtr_user1 CREATE Transaction // 配置另一个数据库连接... CONNECT USING gtr_user1;用完销毁DESTROY gtr_user1AutoCommit False意味着框架不会自动提交——开发者显式写COMMIT或ROLLBACK事务边界清晰可控。今天对应什么SQLCA≈ Spring 的DataSourcePlatformTransactionManagerAutoCommit False 手动COMMIT≈Transactional注解声明事务边界CREATE Transaction创建独立事务 ≈ Spring 的TransactionTemplate编程式事务PB 把连接管理和事务控制提到了框架级别。开发者不碰连接只管逻辑。问题三窗口之间怎么传数据第一性需求一个应用有几十个窗口。用户在窗口 A 输入了参数点按钮打开窗口 BB 要用 A 的参数。怎么办全局变量满天飞写文件中转PB 的回答Message 对象PB 提供一个全局Message对象专门用于窗口间传参// 窗口 A打开窗口 B同时传参数 integer age 50 OpenWithParm(w_to_open, age) // 窗口 B关闭时返回数据给 A CloseWithReturn(Parent, sle_signon_id.Text) // 窗口 A接收返回值 String ls_result Message.StringParm不用全局变量不用写临时文件。OpenWithParm打开窗口的同时把参数塞进 Message目标窗口直接从 Message 取。CloseWithReturn关闭窗口的同时把结果塞回 Message调用方直接读。今天对应什么Message对象 ≈ Vue 的 EventBus / React 的 ContextOpenWithParm≈ React Router 的state参数navigate(/detail, { state: { id: 123 } })CloseWithReturn≈ 弹窗组件的onClose(result)回调思路一样不直接耦合两个组件通过一个中间层传递数据。PB 在 90 年代就用了这个模式。问题四代码怎么组织才不会乱第一性需求项目一大窗口、函数、数据窗口、结构体满天飞。命名混乱不知道谁是谁。三个人协作互相覆盖。PB 的回答命名约定PB 用前缀强制区分类型前缀类型说明w_窗口Windoww_main、w_logindw_数据窗口DataWindowdw_user_listddlb_下拉列表ddlb_deptgf_全局函数Global Functiongf_calc_agest_静态文本StaticTextst_titlecb_按钮CommandButtoncb_save这不是框架强制是社区约定。所有 PB 开发者都这么写。拿到一个陌生项目看前缀就知道每个东西是什么。今天对应什么PB 的前缀约定 ≈ Java 的包命名约定com.company.module.service≈ React 的组件命名约定UserList.jsx、useAuth.js≈ 数据库的字段命名约定user_name、create_time本质都是同一件事约定大于配置。不需要文档名字本身就是文档。问题五怎么调外部系统第一性需求企业系统不是孤岛。社保系统要调银行接口、调医保接口、调第三方加密机。这些外部系统不认你的数据库连接只认 DLL 或 Socket。PB 的回答外部函数声明PB 用FUNCTION关键字直接声明 DLL 接口像调本地函数一样调外部系统// 声明 DLL 函数 FUNCTION int BUSINESS_HANDLE(string InputString, ref string OutputString) LIBRARY SiInterface.dll // 调用 String OutputString OutputString Space(4000) // 预分配内存 BUSINESS_HANDLE(, ref OutputString)注意ref关键字——DLL 修改的是引用传递的参数PB 要预分配好内存空间Space(4000)否则会崩溃。这是 C 语言互操作的基本规则PB 直接暴露给了开发者。今天对应什么DLL 调用 ≈ Java 的 JNIJava Native Interfaceref参数 预分配内存 ≈ C 的指针传参更高层的对应微服务时代的 HTTP/REST API 调用、gRPC 调用PB 时代的集成是进程内 DLL 调用今天是跨进程网络调用。形式变了但问题没变你的系统怎么跟别人的系统说话。问题六批量数据怎么处理第一性需求往数据库插一万条数据逐条 INSERT 太慢。或者要从数据库读出几千条数据逐行处理。怎么高效操作PB 的回答游标 存储过程游标——逐行读取查询结果DECLARE one CURSOR FOR SELECT user_name FROM user_tab ORDER BY user_name; OPEN one; FETCH one INTO :ls_username; DO WHILE SQLCA.SQLCode 0 ddlb_user.AddItem(ls_username) FETCH one INTO :ls_username; LOOP CLOSE one;存储过程——把批量操作推给数据库执行DECLARE Emp_proc PROCEDURE FOR GetName emp_name :Emp_name_var, emp_salary :Emp_sal_var; EXECUTE Emp_proc; FETCH Emp_proc INTO :HostVariableList; CLOSE Emp_proc;PB 的游标语法跟嵌入式 SQL 几乎一样——因为 PB 的设计哲学就是把 SQL 当作一等公民不是字符串拼接而是语言的一部分。今天对应什么游标 ≈ JDBC 的ResultSet逐行遍历存储过程 ≈ MyBatis 的{call procedure_name()}批量操作 ≈ MyBatis 的foreach批量 INSERT总结30 年前和今天问题没变30 年前 PB 解决的问题PB 的方案今天的方案数据全链路查/显/编/校/存DataWindowReact Formik ORM连接和事务管理SQLCASpring Transactional组件间通信Message 对象EventBus / Context代码组织前缀命名约定包命名 / 文件命名约定外部系统集成DLL 声明REST API / gRPC批量数据处理游标 存储过程MyBatis foreach / JDBC Batch框架换了语言换了架构换了。但企业级应用的核心问题——数据怎么流、事务怎么管、组件怎么通信、系统怎么集成——从 90 年代到现在一个都没变。PB 的设计者在 30 年前做出的决策跟今天 Spring 团队、React 团队做出的决策背后是同一套第一性原理。理解了这一点学任何新框架都不会从零开始——因为问题你已经见过了只是解法换了个名字。