☰
老项目维护:InterBase 6.0控件与数据库应用源码拆解
2026/10/27 2:16:51 网站建设 项目流程

简介:InterBase 6.0 是 Borland 推出的关系型数据库管理系统,这份压缩包面向使用 Delphi 6 开发客户端/服务器架构应用的开发者,提供从安装部署、API 编程到示例库测试的完整环境。包内共 130 个文件,约 4.86MB,以 33 个 C 语言源码、20 个示例源码、15 个可执行程序、8 个动态链接库为主体,另有 lib 库文件、h 头文件、hlp 帮助文档、sql 脚本以及 gdb、gbk 示例数据库,覆盖核心引擎调用、安装配置、卸载清理与功能验证等环节。已有 341 人学习,说明其在 Delphi 与 InterBase 技术栈中仍有参考需求。读者可据此搭建本地数据库服务,结合 API 示例理解 SQL 解析、事务处理与连接方式,并借助示例库和帮助文档熟悉触发器、存储过程、视图及权限管理等特性,也便于排查安装依赖与运行环境问题。

1. 一份老项目的现场感:InterBase 6.0 控件与数据库应用源码能解决什么问题

接手一台老工控机上的桌面系统,机器没装任何独立服务,业务数据全在一个几十 MB 的.gdb文件里,程序目录下躺着一份 InterBase 6.0 的客户端运行库,以及一套用控件封装的数据库应用源码。这类项目的价值不在于技术多新,而在于它是完整的、自洽的、能独立跑起来的:单文件数据库、免安装服务端、事务与触发器齐全、配套控件从连接、查询、缓存更新到事件通知一条链全覆盖。适合两类人:手里有类似存量系统需要维护和接管的开发者,以及想通过一份能编译的完整源码,把「控件怎么驱动数据库」这件事看明白的人。下面按目录、代码、脚本、坑位依次拆。

2. 目录结构与运行环境:把 InterBase 6.0 和配套控件跑起来的最小链路

这类资源拿到手,第一件事不是急着编译,而是先看清目录里哪些是运行期文件、哪些是设计期资源、哪些是可以直接执行的数据库脚本。搞混了这两类文件,最常见的后果是程序能打开但连不上库,或者连上了库但控件在 IDE 里报「Class not found」。

2.1 源码包目录拆解:每个文件夹负责哪一段

一份典型的 InterBase 6.0 数据库应用源码包,目录大致长这样:

IB60_Demo/ ├─ bin/ 运行期目录:可执行文件、gds32.dll、ini 配置 │ ├─ DemoApp.exe │ ├─ gds32.dll 客户端连接库,必须与程序位数一致 │ └─ dbconfig.ini 连接参数外置,避免写死在代码里 ├─ db/ 数据库与脚本 │ ├─ DEMO.gdb 示例库文件(可直接附加) │ ├─ 01_create_db.sql 建库脚本:页大小、字符集在这里定 │ ├─ 02_schema.sql 表、生成器、触发器、存储过程 │ └─ 03_init_data.sql 基础数据 ├─ src/ │ ├─ common/ 连接串解析、日志、异常封装 │ ├─ data/ TDataModule:TIBDatabase / TIBTransaction 等 │ ├─ biz/ 业务规则,不直接碰控件 │ └─ ui/ 窗体与交互 ├─ packages/ 控件包与设计期资源(bpl / dcp / pas) ├─ tools/ gbak / gfix / isql 批处理脚本 └─ docs/ 字段说明、接口说明

bin和db是运行期依赖,src是可以改的,packages是设计期依赖,tools是运维脚本。动手之前先确认bin/gds32.dll的位数和你准备用的编译器一致——32 位程序配 64 位客户端库,症状是连接时直接抛「unavailable database」,而且不告诉你原因,这一条后面还会专门讲。

src/data这个目录是整份源码的核心。所有控件实例都在这里集中创建,业务层只调用方法,不直接持有连接对象。这种组织方式在当年的数据库应用里算比较克制的写法,比把TIBQuery拖满每个窗体要好维护得多。看源码时建议先读data目录,把连接和事务的生命周期搞清楚,再看业务。

2.2 建库与连接:端口、页大小、字符集一次定死

服务端跑起来之后,先用isql确认能不能连、库能不能建。这一步不要跳过,很多人直接开程序调试,结果报错信息比命令行含糊十倍。

# 1. 确认服务端在监听默认端口 3050 netstat -an | grep 3050 # 2. 用命令行工具连本机实例,默认管理员账号 SYSDBA isql -user sysdba -password masterkey

进入isql之后建库,页大小和字符集在这一刻就固定下来了,后面想改只能重建库再导数据:

-- 建库语句:页大小、字符集、默认字符集都在这里定 CREATE DATABASE 'D:\IB60_Demo\db\DEMO.gdb' USER 'SYSDBA' PASSWORD 'masterkey' PAGE_SIZE 4096 DEFAULT CHARACTER SET UNICODE_FSS; -- 建完立刻验证字符集 SELECT RDB$CHARACTER_SET_NAME FROM RDB$DATABASE;

PAGE_SIZE决定了一页能放多少行,4096 在中小业务量下是稳妥值,再往上加对单条记录超长、BLOB 较多的场景有好处,但对小记录为主的表收益不明显,反而让每次 I/O 变大。DEFAULT CHARACTER SET是全局默认字符集,纯中文业务如果不想折腾,用UNICODE_FSS;如果历史数据本来就是按单字节存的,用NONE并保证客户端全程同一套编码,反而更省事——代价是所有字符串比较和排序都按字节走。

如果手上只有一个.gdb文件而没有建库脚本,可以用备份工具把库结构「洗」一遍,顺带改页大小:

# 先备份,恢复时指定新的页大小,等价于重建了一个干净的库 gbak -b -v -user sysdba -password masterkey db/DEMO.gdb db/DEMO.gbk gbak -c -v -page_size 4096 -user sysdba -password masterkey db/DEMO.gbk db/DEMO_NEW.gdb

-b是备份,-c是创建恢复,-page_size只在恢复时生效,这是唯一一个不用重建表就能调整页大小的口子,血泪经验,记下来。

2.3 编译前核对:控件包、运行库与 IDE 搜索路径

控件安装这一步在现在的 IDE 上会很别扭,但在当年的环境里就是标准流程:把packages目录里的.pas源文件加进库路径,把.bpl装进 IDE,然后在组件面板上能看到数据库页签才算成功。

检查项正确状态出错时的表现
客户端库位数与编译目标一致连接抛 unavailable database
控件源文件路径已加入搜索路径编译报找不到 TIBDatabase
设计期包已在 IDE 中注册组件面板没有数据库页签
运行时库与 exe 同目录或系统路径启动即报模块加载失败
连接配置指向实际.gdb绝对路径报 I/O error for file

连接参数不要写死在代码里,外置成一个 ini,换机器只改这个文件:

[Database] Path=D:\IB60_Demo\db\DEMO.gdb User=SYSDBA Password=masterkey Charset=UNICODE_FSS

Path用绝对路径,别用相对路径。服务端解析路径时以服务端进程的工作目录为基准,不是以你的 exe 所在目录为基准,本机开发时两者恰好相同所以看不出问题,一部署到别的机器立刻翻车。

3. 控件层拆解:一个查询从 TIBDatabase 到 TIBQuery 的完整链路

控件这套东西的本质是把「连接、事务、语句、结果集」四件事拆成四个组件,然后靠属性互相引用串起来。串错一根线,症状往往是数据出不来、写不进去、或者事务莫名其妙锁住。这一章按数据流动的顺序拆。

3.1 连接与事务:两个组件必须成对管理

连接对象负责物理链路,事务对象负责可见性边界,两者是多对一的关系:一个连接可以挂多个事务,但绝大多数项目里一个默认事务就够。初始化代码集中在数据模块的OnCreate里最简单:

// 数据模块初始化:连接对象 + 默认事务对象 procedure TDMData.DataModuleCreate(Sender: TObject); begin FDB := TIBDatabase.Create(Self); FDB.DatabaseName := ReadIni('Database', 'Path', 'DEMO.gdb'); FDB.Params.Clear; FDB.Params.Add('user_name=' + ReadIni('Database', 'User', 'SYSDBA')); FDB.Params.Add('password=' + ReadIni('Database', 'Password', 'masterkey')); FDB.Params.Add('lc_ctype=' + ReadIni('Database', 'Charset', 'UNICODE_FSS')); FDB.LoginPrompt := False; // 绝不弹登录框,密码从配置来 FDB.Connected := True; FTran := TIBTransaction.Create(Self); FTran.DefaultDatabase := FDB; FTran.Params.Clear; FTran.Params.Add('read_committed'); // 读已提交 FTran.Params.Add('rec_version'); // 读已提交时保留记录版本 FTran.Params.Add('nowait'); // 冲突立即报错,不排队等待 FTran.Active := True; end;

Params里那三行是关键。read_committed表示事务只能看到别人已经提交的数据,配rec_version才能读到最新版本;nowait表示遇到被别人锁住的行立刻抛异常,而不是挂在那里等。nowait的选择要看你有没有统一的冲突重试逻辑,没有的话尽量改成等待模式(去掉nowait),否则用户点一下保存就弹一个错误框,体验很差。

连接打开后先跑一条轻量语句验证链路,比只看Connected属性靠谱:

// 连接健康检查:能查到版本号才算真的通 function TDMData.Ping: Boolean; var Q: TIBSQL; begin Result := False; Q := TIBSQL.Create(nil); try Q.Database := FDB; Q.Transaction := FTran; Q.SQL.Text := 'select rdb$get_context(''SYSTEM'', ''ENGINE_VERSION'') from rdb$database'; Q.ExecQuery; Result := not Q.Eof; finally Q.Free; end; end;

TIBSQL是轻量级的单语句执行组件,不返回可编辑数据集,做健康检查、执行 DDL、跑批量更新都比TIBQuery干净。Database和Transaction两个属性必须都赋,只赋前者会在执行时报「transaction not active」。

3.2 参数化查询与缓存更新:TIBUpdateSQL 的正确接法

查询本身没有难度,难的是把「改了多行再统一提交」这件事做对。控件层的做法是缓存更新:先把结果集读到内存,用户改完,ApplyUpdates一次性生成对应的增删改语句。

// 参数化查询:永远用参数,不要拼字符串 procedure TfrmOrder.LoadOrders(const ACustomerId: Integer); begin qryOrder.Close; qryOrder.SQL.Text := 'select ORDER_ID, CUSTOMER_ID, AMOUNT, STATUS, UPDATED_AT' + ' from ORDERS where CUSTOMER_ID = :CUSTOMER_ID order by ORDER_ID'; qryOrder.Params.ByName('CUSTOMER_ID').AsInteger := ACustomerId; qryOrder.Open; end;

为什么强调参数化:一是防注入,二是让服务端能复用执行计划,三是避免日期、数值在不同区域设置下拼出错误的字面量。字段名和表名无法参数化,这部分如果来自用户输入,必须走白名单。

缓存更新需要给每条语句配一个更新对象,关键是主键条件用OLD_前缀引用原始值:

// 缓存更新:查询与写回分离 procedure TfrmOrder.SetupCachedUpdate; begin qryOrder.CachedUpdates := True; qryOrder.UpdateObject := updOrder; // TIBUpdateSQL 组件 updOrder.ModifySQL.Text := 'update ORDERS set AMOUNT = :AMOUNT, STATUS = :STATUS,' + ' UPDATED_AT = current_timestamp' + ' where ORDER_ID = :OLD_ORDER_ID'; updOrder.InsertSQL.Text := 'insert into ORDERS (ORDER_ID, CUSTOMER_ID, AMOUNT, STATUS, UPDATED_AT)' + ' values (:ORDER_ID, :CUSTOMER_ID, :AMOUNT, :STATUS, current_timestamp)'; updOrder.DeleteSQL.Text := 'delete from ORDERS where ORDER_ID = :OLD_ORDER_ID'; end; // 提交:先落库,再提交事务,顺序不能反 procedure TfrmOrder.SaveOrders; begin if qryOrder.UpdatesPending then qryOrder.ApplyUpdates; // 逐行执行上面三条语句 FTran.Commit; // 事务边界在这里,不在 ApplyUpdates 里 qryOrder.CommitUpdates; // 清掉缓存标记,否则下次还会重放 end;

OLD_ORDER_ID里的OLD_是框架约定的前缀,代表「改之前的值」。如果只写:ORDER_ID,取到的是当前(改后)的值,主键一被修改就更新不到任何行,而且不会报错,ApplyUpdates返回影响行数为 0,数据静默丢失——这是缓存更新里最阴的一类问题。建库时给主键加唯一约束、提交后核对影响行数,能提前把它暴露出来。

ApplyUpdates本身不开事务也不提交事务,它只是在当前事务里执行语句。所以要么在调用前确保事务是活跃的,要么用StartTransaction显式开一段。CommitUpdates也别忘了,它的作用是把已成功写回的记录从缓存里清掉,漏了这一句,用户再点一次保存会把同样的更新重放一遍。

3.3 存储过程、事件通知与 BLOB 字段的读写

统计类逻辑放服务端比拉回客户端算要快得多,也避免了「客户端版本不一致导致算法不同」的问题。存储过程在控件层的调用很直接:

// 调用返回单值的存储过程 procedure TfrmStat.CalcTotal(const ACustomerId: Integer); begin spTotal.StoredProcName := 'SP_CUSTOMER_TOTAL'; spTotal.Params.ByName('PCUSTOMER_ID').AsInteger := ACustomerId; spTotal.ExecProc; lblTotal.Caption := FormatFloat('#,##0.00', spTotal.Params.ByName('TOTAL').AsFloat); end;

StoredProcName要在Params之前设,因为参数列表是打开时从服务端元数据里读的。如果先设参数后设名字,参数会被清空。

事件通知是这套东西里被严重低估的功能,用好了能省掉一半轮询。服务端任意一个连接执行post_event,所有注册了同名事件的客户端都会收到异步回调:

// 事件注册:多个事件名用换行分隔 procedure TDMData.InitEvents; begin IBEvents.Events.Text := 'ORDER_CHANGED'; IBEvents.RegisterEvents; // 主动注册,不是设个属性就完事 end; // 回调在非主线程触发,更新界面必须切回主线程 procedure TDMData.IBEventsEventAlert(Sender: TObject; EventName: string; EventCount: Integer); begin TThread.Queue(nil, procedure begin if Assigned(FOnDataChanged) then FOnDataChanged(EventName); end); end;

EventCount是两次投递之间累计的次数,不是序号。做「有变化就刷新」的语义时只看它大于零即可,别拿它当计数器用。另外事件不保证不丢,网络断开重连期间的投递会丢,所以它只能用来触发刷新,不能当作可靠消息队列。

BLOB 字段的读写要用流,别用字符串:

// 读 BLOB:用 TIBBlobStream,避免整块读进内存 procedure TfrmDoc.LoadAttachment(const ADocId: Integer); var BS: TIBBlobStream; begin qryDoc.Close; qryDoc.SQL.Text := 'select CONTENT from DOCS where DOC_ID = :ID'; qryDoc.Params.ByName('ID').AsInteger := ADocId; qryDoc.Open; BS := TIBBlobStream.Create; try BS.Mode := bmRead; BS.Database := FDB; BS.Transaction := FTran; BS.Blob := qryDoc.FieldByName('CONTENT') as TBlobField; mmoContent.Lines.LoadFromStream(BS); // 流式读取,长度由控件负责 finally BS.Free; end; end;

Mode设为bmRead时是只读流,写回要用bmWrite并配合Post。BLOB 的读写必须在同一个活跃事务里完成,事务一提交,流指针就失效了。文本类 BLOB 用子类型 1(TEXT),二进制附件用子类型 0,子类型建错了会导致客户端按文本解析二进制内容,直接乱码。

4. 服务端逻辑与运维脚本:PSQL、gbak、gfix 在项目里的落点

客户端控件再熟,也架不住服务端设计得糙。这一章讲三件事:主键怎么生成、备份怎么做、建库参数怎么选。这三件事在项目上线前定好,后面几年都省心。

4.1 生成器、触发器与存储过程的写法约定

自增主键用生成器加触发器,这是这套数据库里最标准的做法,不要用max(id)+1:

-- 生成器:一个自增序列 CREATE GENERATOR GEN_ORDER_ID; SET GENERATOR GEN_ORDER_ID TO 10000; -- 触发器:插入前自动填主键 CREATE TRIGGER TR_ORDERS_BI FOR ORDERS ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.ORDER_ID IS NULL) THEN NEW.ORDER_ID = GEN_ID(GEN_ORDER_ID, 1); END

GEN_ID(生成器名, 步长)是原子操作,多客户端并发插入不会撞号,但它不参与事务回滚——事务回滚了号照样被消耗掉,所以主键会出现空洞。这属于正常现象,不要为了「连续编号」去改写生成器逻辑,那会引入并发冲突。

存储过程里要区分两种:返回结果集的用SUSPEND,返回单值的用INTO加SUSPEND。

SET TERM ^ ; -- 返回单值的统计过程 CREATE PROCEDURE SP_CUSTOMER_TOTAL (PCUSTOMER_ID INTEGER) RETURNS (TOTAL NUMERIC(18,2)) AS BEGIN TOTAL = 0; SELECT SUM(AMOUNT) FROM ORDERS WHERE CUSTOMER_ID = :PCUSTOMER_ID AND STATUS = 'PAID' INTO :TOTAL; IF (TOTAL IS NULL) THEN TOTAL = 0; SUSPEND; END ^ SET TERM ; ^

SET TERM ^是必须的,因为过程体内部用分号分隔语句,客户端解析器会把第一个分号当成语句结束。改成^作终止符之后,END ^才表示整个过程结束。忘了这一句,报的错是「unexpected end of file」,跟真正的原因完全对不上。INTO :TOTAL里的冒号表示「这是变量不是字段」,漏了冒号会被当成字段名去解析。

4.2 备份恢复与一致性校验的脚本化

备份只有一条铁律:定期跑、跑完验证、验证失败要告警。手工点图形界面工具的备份,三个月后一定会忘。

#!/bin/bash set -euo pipefail DB=/data/ib/DEMO.gdb BAK=/backup/DEMO_$(date +%Y%m%d_%H%M).gbk LOG=/var/log/ib_backup.log # 1. 结构校验:只读,能提前发现页损坏 gfix -v -full -user sysdba -password masterkey "$DB" >> "$LOG" 2>&1 # 2. 逻辑备份:-g 跳过垃圾回收,-v 输出进度 gbak -b -v -g -user sysdba -password masterkey "$DB" "$BAK" >> "$LOG" 2>&1 # 3. 恢复验证:恢复到临时库,成功了才认为备份可用 TMP=/tmp/verify_$$.gdb gbak -c -v -user sysdba -password masterkey "$BAK" "$TMP" >> "$LOG" 2>&1 rm -f "$TMP" echo "backup ok: $BAK" >> "$LOG"

第 3 步是很多人省略、出事时最后悔的一步。备份文件本身也可能是坏的,只有真正恢复一次才知道能不能用。恢复到临时库再删掉,代价是几分钟磁盘 I/O,换的是「备份真的可用」这个结论。

-g的含义是备份时不带垃圾记录,产出的文件更小,恢复后库更紧凑。但它要求备份期间库处于一致状态,业务高峰跑备份容易拿到半新半旧的数据,稳妥做法是先短暂禁止新连接:

# 强制断开现有连接并禁止新连接,备份完再放开 gfix -shut -force -t 30 -user sysdba -password masterkey /data/ib/DEMO.gdb # ... 执行上面的备份 ... gfix -online -user sysdba -password masterkey /data/ib/DEMO.gdb

-t 30是等待秒数,超时后强制断开。这段窗口期内所有客户端都会报连接失败,所以只能放在维护时段跑,别配成定时任务每小时执行一次。

4.3 页大小与字符集:建库时定生死

这两个参数建库后无法原地修改,只能备份恢复重建,所以在项目启动时就要想清楚。

参数可选值选择建议
页大小1024 / 2048 / 4096 / 8192中小记录用 4096;大字段、宽表用 8192
字符集NONE / ASCII / UNICODE_FSS有中文且需排序比较用 UNICODE_FSS
排序规则跟随字符集中文排序建议客户端排,服务端只做等值匹配
BLOB 子类型0 二进制 / 1 文本附件用 0,富文本用 1

UNICODE_FSS的代价是每个字符占 3 字节(变长存储),索引体积比单字节字符集大不少。当年很多项目为了省空间选了NONE,然后靠客户端统一按本地编码读写,只要全链路编码一致也能跑,但一旦有人用别的工具连进来查数据就是乱码,排查起来极其费劲。新项目直接上UNICODE_FSS,别省这点空间。

5. 避坑与排查:这套控件组合最容易翻车的五个地方

下面五条都是我在实际项目里撞过的,按「现象 → 原因 → 解决」写,遇到同类症状可以直接对号入座。

5.1 中文存进去变问号,或者读出来是乱码

现象:程序里输入中文,保存成功不报错,重新查询显示成一串问号;或者用命令行工具查同一张表,数据是好的,程序里读出来是乱码。

原因:字符集在三个地方各自为政——建库时的DEFAULT CHARACTER SET、连接参数里的lc_ctype、客户端字符串的编码。三者不一致时,服务端会做一次「尽力而为」的转换,转不了就替换成占位符,且不报错。最常见的是库建成NONE,连接参数写了UNICODE_FSS,服务端按字节直传,客户端按双字节解析,直接错位。

解决:先把三处对齐再谈其他。用isql查一下库的真实字符集,把连接参数里的lc_ctype改成和库一致;如果库是NONE且历史数据是单字节存的,客户端就不要设lc_ctype,让字节原样进出,界面上按本地编码显示。已经写坏的数据救不回来,只能从备份恢复后重录,所以这一条要在开发第一天验证,别等到上线。

5.2 保存时抛 lock conflict on no wait transaction

现象:单用户测试一切正常,多用户同时操作同一张单据时,其中一方弹「lock conflict on no wait transaction」,重试一次又好了。

原因:事务参数里带了nowait,遇到目标行被别人的未提交事务锁住时立即报错,不排队。而业务代码里往往是一个事务从打开窗体一直开到关闭窗体,中间用户去泡了杯咖啡,锁就一直挂着。

解决:两条路一起走。短期把nowait去掉,让服务端排队等待,配一个合理的锁等待超时;长期把事务粒度收窄,改成「读的时候一个短事务,写的时候开一个事务、写完立刻提交」,绝不让事务跨越用户交互。我一般的做法是在数据模块里封装BeginWork/CommitWork/RollbackWork三个方法,业务层只在这三个方法之间操作,谁手动Active := True一律代码评审打回。

5.3 开发机好好的,换台机器就连不上

现象:程序在自己机器上跑得好好的,拷到客户机器上启动就报「unavailable database」或者「cannot attach to services manager」,错误信息里没有任何有用线索。

原因:三种可能混在一起。一是gds32.dll位数或版本不匹配,32 位程序加载了 64 位库;二是客户端库不在搜索路径里,或者被系统目录里另一个版本的库抢先加载;三是服务端没监听 3050,或者本机防火墙拦了出站。

解决:按顺序排查。先确认gds32.dll和 exe 都在同一目录且位数一致,用文件属性看;再把 dll 放到 exe 同目录而不是系统目录,避免版本互相覆盖;最后在客户机上用netstat确认 3050 可达。另外,如果连的是远程实例,连接串里要写主机名:路径的形式,只写路径会被当成「本机服务端上的路径」,这条在部署文档里必须写清楚。

5.4 备份文件恢复时报错,或者恢复出来的库缺数据

现象:每天定时备份,文件大小也正常,真出事要恢复时,gbak -c报校验失败;或者恢复成功,但最后几分钟的业务数据不见了。

原因:备份期间库还在被写入,拿到的是一致性快照,快照之后提交的数据自然不在里面;如果备份进程和写进程抢页,还有概率拿到损坏的页。另外备份文件没做恢复验证,坏了也不知道。

解决:备份前用gfix -shut短暂禁止连接,备份完再-online放开,把窗口期压到几分钟;备份脚本里固定加一步「恢复到临时库再删掉」的验证;备份文件异地存一份,别和库文件放同一个磁盘。定时任务里如果set -e生效,gfix -v返回非零会中断整个脚本,所以要单独处理它的返回码,别让它挡住后面的备份。

5.5 提交事务时报 transaction is active,或者数据「凭空消失」

现象:调用Commit时报「transaction is active」,或者明明插入成功了,另一个客户端查不到。

原因:前者是数据集还开着就提交事务,游标没关,服务端不允许在活跃游标上提交;后者是隔离级别选成了快照读,别的客户端在一个长事务里,看不到你新提交的数据。

解决:提交前先Close所有关联的数据集,或者把数据集改成非活跃;隔离级别按场景选,需要看到别人新提交的数据就用读已提交,需要事务内多次读取结果一致就用快照读,别一把梭全用同一种。判断「数据是不是真的写进去了」,用另一个新连接去查,别在同一个事务里自证。

6. 二次开发改造:把示例代码收成一个可复用的数据访问层

示例源码能跑,但直接在上面加功能会很快失控:几十个窗体各自持有查询控件,事务边界散落各处,错误处理靠弹框。改造的第一步不是换技术栈,是把数据访问收口到一个薄层里,让业务代码只描述「要什么」,不描述「怎么连」。

具体做法是定义一组接口方法,内部统一管理连接、事务和异常映射:

// 数据访问层:业务只关心结果,不关心控件 function TDataAccess.QueryInt(const ASQL: string; const AParams: array of Variant): Integer; var Q: TIBSQL; I: Integer; begin Q := TIBSQL.Create(nil); try Q.Database := FDB; Q.Transaction := FTran; Q.SQL.Text := ASQL; for I := 0 to High(AParams) do Q.Params[I].Value := AParams[I]; Q.ExecQuery; Result := Q.FieldByName('V').AsInteger; finally Q.Free; end; end;

参数用array of Variant传,调用侧写成QueryInt('select count(*) as V from ORDERS where STATUS = ?', ['PAID']),比每次 new 一个控件再设属性短得多。内部固定走TIBSQL,不返回可编辑数据集,逼着业务层用显式语句而不是拖控件绑定。

第二步是把错误码映射成业务语义。控件的异常对象里带着服务端返回的错误码,直接把它弹给用户没意义,要转成能指导操作的话:

错误码段含义建议动作
335544336死锁自动重试一次,仍失败则提示稍后再试
335544345锁冲突提示记录已被他人修改,请刷新
335544569动态 SQL 语法错误写日志,属于代码缺陷
335544321数据类型转换失败提示检查输入格式

第三步是验证。改完不能只看功能对不对,要压一遍并发:开两个客户端,同时改同一行,确认一个成功一个给出明确提示而不是卡死;再跑一次备份恢复,确认改造没引入新的对象依赖;最后把tools目录里的脚本接到定时任务,观察一周日志里有没有新的错误码。

从那以后我每次接这类存量项目,都强制自己先花半天把连接、事务、备份三件事的现状跑一遍,再去读业务代码。跳过这一步直接改功能,后面总要用加班还回来。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询