简介:面向Delphi与C++Builder开发者的ODAC组件集,专注连接和操作Oracle数据库,能有效解决连接管理、SQL查询、事务控制、存储过程调用等常见问题。压缩包共含1807个文件,整体大小约9.7MB,以源码、编译单元、窗体文件和组件包工程为主,另含项目文件、运行库、配置与SQL脚本,目录组织较清晰,便于直接编译、安装或对照学习。包内提供核心连接控件,以及查询、表格、事务、存储过程等组件,支持连接池、异步模式、错误处理和Oracle各版本兼容。通过源码与示例,可掌握连接参数配置、游标批处理、LOB字段读写、PL/SQL调用、性能优化等实用技巧,组件调用方式清晰,在真实项目中可直接复用,减少从零搭建的重复工作。目前已有466人学习/下载,对于需要构建Oracle数据库应用的开发者,是一份兼顾学习与工程参考价值的资源。
1. Delphi 连 Oracle 为什么选 ODAC:一套连接控件解决的部署与性能问题
做 Delphi 的老开发,只要接 Oracle,基本都会在某个时刻被 BDE、SQL*Net 的繁琐配置逼到换控件。ODAC(Oracle Data Access Components)就是把连接、查询、存储过程、批量导入这些活都收敛到一套 Delphi 原生控件里的连接控件,核心是 TOraSession,配合 TOraQuery、TOraStoredProc、TOraScript 使用。它最常用的两种工作方式是直接模式(Direct)和 OCI 模式:前者不用装 Oracle 客户端,后者走官方 OCI 接口,性能和功能更完整。这篇笔记会把安装、连接串、数据操作、发布部署和常见监听器/NLS/内存错误一次讲透。适合正在维护老 Delphi 项目、或者要从 BDE 迁移到 Oracle 的工程师。
2. 从安装到连通:TOraSession 的连接串参数、直接/OCI 模式怎么选
2.1 安装 ODAC 的版本匹配问题
ODAC 不是单个 pas 文件,而是一组设计时和运行时包。装的时候第一步就要对 Delphi 版本。Delphi XE2 到 12、RAD Studio 11/12,安装包按平台单独分发;装错版本最典型的后果是 IDE 提示 can't load package,或者运行时 BPL 加载报错。我一般习惯在正式项目动手前,先开一个空项目,只放一个 TOraSession 验证 IDE 能编译过,再往老项目里引,这个前置验证能筛掉一半环境问题。
安装完成后组件面板会多一个 ODAC 页,TOraSession 排在第一个。这一步有几个经常翻车的点:一是下载包解压后没有先执行安装脚本,直接拷 DLL 到 System32,导致 IDE 识别不到设计时包;二是旧的 ODAC 版本残留注册表信息,与新版包冲突。建议先卸载干净,再按安装向导选择当前 Delphi 版本,安装过程的目录选项我一般保持默认,不去手动改。需要注意,安装向导会让选择默认支持直接模式还是 OCI 模式,这个选择只影响默认属性,运行时仍然可以随时切换。
2.2 ConnectString、SID 与服务名的参数区别
连接 Oracle 的必备信息是服务器地址、端口、数据库标识。ODAC 的连接控件有 ConnectString,也可以用 Server、UserName、Password 等独立属性。这里的坑是 Oracle 有两种标识,SID 和 SERVICE_NAME,格式不能混用。经典的连接串写法如下:
OraSession1.ConnectString := 'Server=192.168.1.20:1521:ORCL;SID=ORCL;Direct=True;';这里 1521 是监听端口,ORCL 是 SID。如果目标库是 Oracle 12c 以后的 PDB,常用的是服务名,要换成斜杠分隔的格式:
OraSession1.ConnectString := 'Server=192.168.1.20:1521/orclpdb;ServiceName=orclpdb;Direct=True;';分号和冒号的位置是 ODAC 解析的关键。ServiceName 格式用IP:端口/服务名,SID 格式用IP:端口:SID,写错了解析会失败。在 OCI 模式下,还可以不写 Server,只给 HomeName 和 Net 别名走 TNSNAMES,但那样会增加环境依赖,客户端配置一变就难排查。我自己的习惯是项目里永远显式写 IP、端口和 SID/服务名,不给玄学空间。
连接串里还有几个实用参数:HomeName 指定 OCI 主目录,Pooling=True/False 控制连接池,UseUnicode=True/False 控制字符集。UseUnicode 这里先提一句:数据库端字符集如果是 AL32UTF8,不开 Unicode 会出现转换异常;开了则字符串以 Unicode 传递,内存占用略高。后面乱码章节会展开讲。
2.3 用代码建立连接并验证会话
连接控件可以纯代码创建,也可以从组件面板拖到 DataModule。为了演示最小可用链路,我写一个按钮事件,创建 TOraSession,设置直接模式,连上后取服务器版本:
uses Ora; procedure TForm1.btnConnectClick(Sender: TObject); var LSess: TOraSession; begin LSess := TOraSession.Create(nil); try LSess.Options.Net := nnDirect; // nnDirect 走直接模式,nnOCI 走 OCI LSess.ConnectString := 'Server=192.168.1.20:1521:ORCL;SID=ORCL;Direct=True;'; LSess.UserName := 'scott'; LSess.Password := 'tiger'; LSess.Connect; Caption := 'Connected ' + LSess.ServerVersion; finally LSess.Free; end; end;这段代码的要点:Options.Net 决定连接实现,直接模式不需要 Oracle 客户端库。ConnectString 里的 Direct=True 和 Options.Net := nnDirect 是同一件事的两处表达,ODAC 以 Options.Net 为准,ConnectString 里的 Direct 参数在部分版本里只用于初始化。ServerVersion 属性在连接成功后才有效,拿来做连接验证很直观。
注意,Create(nil) 创建的临时会话用完后必须 Free。实际项目里我会把 TOraSession 放在 DataModule 上长期持有,而不是每个按钮新建一次连接,否则每次操作都有连接建立开销,交互会很慢。
2.4 直接模式与 OCI 模式:怎么选才不后悔
直接模式和 OCI 模式是 ODAC 最核心的选型。直接模式由 ODAC 自己实现 Oracle 网络协议,客户端不需要安装 Oracle 或 Instant Client,发布时拷贝少量文件就能跑,对外部环境依赖最少。它的边界在高级功能:不像 OCI 那样支持 Oracle 对象类型、高级队列、部分分布式事务,NLS 处理也要靠 ODAC 自己的参数配置。
OCI 模式则通过 Oracle 官方客户端库连接,能继承 Oracle 客户端的全部能力,对复杂类型和大结果集处理更可靠,但发布时要带 OCI 相关 DLL,还受 TNSNAMES、LD_LIBRARY_PATH、注册表等环境影响。我在维护老 ERP 项目时遇到的情况是:开发机用 OCI 一切正常,到客户现场因为少了一个 oci.dll 直接起不来,后来改成直接模式才消停。
结论不绝对。做原型、工具类小系统、快速交付的,直接模式足够;重事务、多功能类型、必须用 Oracle 特定特性的,选 OCI 并把客户端依赖写进部署文档。ODAC 的 Options.Net 在运行时可以切换,所以初版先用直接模式把业务跑通,遇到功能边界再切换,两手都不误。
3. 跑通数据操作:TOraQuery 绑定变量、REF CURSOR 与事务提交
3.1 TOraQuery 绑定变量写法
TOraQuery 对应熟悉的 TQuery/TDataSet,SQL 文本里的:参数名就是绑定变量。用绑定变量而不是把值拼进字符串,一方面防 SQL 注入,另一方面让 Oracle 能复用游标,减少硬解析。
OraQuery1.SQL.Text := 'SELECT ENAME, SAL FROM EMP' + ' WHERE DEPTNO = :DeptNo AND SAL > :MinSal'; OraQuery1.ParamByName('DeptNo').AsInteger := 10; OraQuery1.ParamByName('MinSal').AsFloat := 2000; OraQuery1.Open; while not OraQuery1.Eof do begin Memo1.Lines.Add( OraQuery1.FieldByName('ENAME').AsString + #9 + OraQuery1.FieldByName('SAL').AsString); OraQuery1.Next; end;ODAC 对:DeptNo的处理是生成 Oracle 绑定变量,值类型由 AsInteger/AsFloat 决定。需要注意,ParamByName 在参数不存在时会直接抛异常,所以 SQL 文本里的参数名必须和代码里严格一致,大小写不敏感但拼写必须对。还有一个易错点:同一条 SQL 里同一个参数只出现一次,如果要在 SELECT 和 WHERE 同时用,要写两个不同名字的参数,这和 Oracle 原生绑定变量的行为一致。
结果集遍历用 Eof/Next 是常见做法;如果只取单值,可以用 FieldByName 直接取值。大数据量场景不要一边遍历一边做界面刷新,先攒到内存列表再一次性刷,否则 UI 会卡到像死机一样。
3.2 FetchAll 与分页查询
默认情况下 TOraQuery.Open 会把结果集全部拉到本地,小表没问题,几十万行会把内存吃满。ODAC 提供 FetchAll 属性,置为 False 后每次从服务器取一批行,配合 FetchRows 控制批量大小。大数据量报表我会把 FetchAll 关掉,然后按页取数:
OraQuery1.FetchAll := False; OraQuery1.FetchRows := 500; OraQuery1.SQL.Text := 'SELECT * FROM (SELECT t.*, ROWNUM RN FROM (' + ' SELECT ENAME, SAL FROM EMP ORDER BY SAL DESC' + ') t WHERE ROWNUM <= :PageEnd) WHERE RN >= :PageStart'; OraQuery1.ParamByName('PageEnd').AsInteger := 100; OraQuery1.ParamByName('PageStart').AsInteger := 1; OraQuery1.Open;这是经典的三层嵌套分页:先排序,再取前 N 行,最后按 ROWNUM 过滤起始位置。PageEnd/PageStart 是页码换算后的行号。排序字段必须有唯一性,否则翻页时偶发重复行,这是 Oracle 分页的老问题。Oracle 12c 以上可以用 OFFSET ROWS FETCH NEXT 更简洁,但老库上普遍用 ROWNUM 写法。
FetchRows 是每次网络往返取回的行数,设太大首次等待长,设太小交互频次高。我一般按单行大小调整,窄表 1000,宽表 200 到 500。还有一条血泪经验:FetchAll=False 时,不要在遍历结果集的同时执行另一条会改变连接的 SQL,否则前一个游标会被隐式关闭,拿到的是不完整数据。
3.3 调用 Oracle 存储过程和 REF CURSOR 返回
ODAC 有两种方式调存储过程。一种直接用 TOraStoredProc 组件,按名字映射参数;一种在 TOraQuery 里写匿名块。我更喜欢后者,逻辑集中在一个 SQL 文本里,方便复制到 PL/SQL Developer 调试。先看 TOraStoredProc:
OraStoredProc1.StoredProcName := 'PKG_EMP.GET_EMP_BY_DEPT'; OraStoredProc1.ParamByName('P_DEPTNO').AsInteger := 10; OraStoredProc1.Open;只要过程定义里的出参是 SYS_REFCURSOR,ODAC 会自动把游标映射为数据集,Open 后就能按字段遍历。这个自动映射的关键是参数方向必须正确:过程里 P_CURSOR OUT SYS_REFCURSOR,ODAC 的对应参数类型是 ftCursor,方向为输出。如果 Open 后数据集是空的,先查参数方向是不是被建成了输入,这是最常见的原因。
用匿名块方式可以动态指定游标变量名,代码更灵活:
OraQuery1.SQL.Text := 'BEGIN PKG_EMP.GET_EMP_BY_DEPT(:P_DEPTNO, :C_EMP); END;'; OraQuery1.ParamByName('P_DEPTNO').AsInteger := 10; OraQuery1.ParamByName('C_EMP').DataType := ftCursor; OraQuery1.ParamByName('C_EMP').ParamType := ptOutput; OraQuery1.Open;这里 C_EMP 必须显式声明为输出游标参数,否则 ODAC 无法识别出参类型。另一个坑:匿名块里的参数名如果和过程内部变量同名,会被 PL/SQL 变量遮蔽,报变量未声明的错误,处理办法是给入参起别名,或者在过程签名里统一用 p_ 前缀。存储过程内部的异常默认会向上抛到 Delphi 端,捕获后用 OraSession.ErrorCode 和 ErrorMessage 可以拿到 ORA 错误码,比只看弹窗信息有用得多。
3.4 事务控制与批量提交
ODAC 的连接控件默认不是自动提交,这一点比很多桌面数据库要严格。建议显式控制事务:连接后设 AutoCommit := False,一组 DML 完成后 Commit,出现异常就 Rollback。
OraSession1.AutoCommit := False; try OraQuery1.SQL.Text := 'UPDATE EMP SET SAL = SAL * 1.1 WHERE DEPTNO = :D'; OraQuery1.ParamByName('D').AsInteger := 10; OraQuery1.ExecSQL; OraQuery2.SQL.Text := 'INSERT INTO EMP_LOG(DEPTNO, OPER, LOG_TIME) ' + 'VALUES (:D, ''RAISE'', SYSDATE)'; OraQuery2.ParamByName('D').AsInteger := 10; OraQuery2.ExecSQL; OraSession1.CommitRetaining; except OraSession1.Rollback; raise; end;CommitRetaining 是 ODAC 提供的提交后保持事务上下文的方法,提交后游标和锁定状态不释放,适合循环分批提交的场景。如果一次性处理上万行,建议每 500 行提交一次,不要攒到最后一次性提交,避免 UNDO 段膨胀。还有一个经验:批量 UPDATE 前先确认 WHERE 条件能命中索引,否则行锁会放大,ODAC 端表现为提交时长时间卡住,等半天才发现是全表扫描。
4. 连接排查清单:监听、NLS 乱码与 Delphi 内存错误
4.1 ORA-12518:监听无法分发连接的典型原因
现象:ODAC 连接时报ORA-12518: TNS:listener could not hand off client connection。
原因多半不是 ODAC 配置,而是数据库端进程或会话耗尽。Oracle 的 processes 参数决定了最大后台进程数,监听把连接分发给专用服务器进程时发现没有可用进程,就会报 12518。服务器内存不足也会出现同样错误。
解决:先用 SYS 账号登录数据库查参数使用率。
SELECT name, value FROM v$parameter WHERE name = 'processes'; SELECT COUNT(*) FROM v$process; SELECT COUNT(*) FROM v$session;v$process 数量接近 v$parameter 里 processes 上限,就说明进程被占满。常见做法是ALTER SYSTEM SET processes = 300 SCOPE = SPFILE,然后重启实例。更可持续的办法是查一下哪些会话长期挂在 INACTIVE,通常是应用端连接没释放,关掉那几个会话再观察。
ODAC 侧能做的有两点:一是连接池最大连接数不要超过数据库 processes 余量;二是程序关闭时确保 TOraSession 被 Free,否则应用端留下的死连接要等 TCP 超时才能被 Oracle 回收,日积月累就把会话池堆满了。
4.2 监听服务起不来:端口、hosts 与 listener.ora
现象:Oracle 的监听服务无法启动,事件日志里提示监听启动失败。这个坑在 Windows 上尤其典型,排查顺序固定:先看端口占用,再看 hosts 解析,最后看 listener.ora。
netstat -ano | findstr 1521如果有 LISTENING 的进程占着端口,说明可能装了多个 Oracle 实例或残留后台进程,杀掉旧进程再启动监听。hosts 文件里机器名解析不到本机 IP,监听起来后外部连不上,会表现为连接超时而不是连接拒绝,容易被误导。listener.ora 里如果配了错误的 HOST,监听只挂在不对的地址上,用lsnrctl status能看到 Listen 地址,不是对应 IP 就改 listener.ora。
ODAC 侧面对这个现象时不要急着改代码:先用 PL/SQL Developer 或者 sqlplus 在目标机器上试连服务器,如果 sqlplus 也连不上,那就是网络和监听问题,不是控件问题。这条经验能省掉一半的 Delphi 调试时间。
4.3 中文乱码:NLS_LANG 与 ODAC 的 EnableNLS
现象:从 Oracle 读出的中文变成问号或者乱码。
原因通常是字符集链路不一致:数据库字符集、Oracle 客户端 NLS_LANG、ODAC 的 Unicode 开关三者中有一个不对。先确认库端字符集:
SELECT value FROM v$nls_parameters WHERE parameter = 'NLS_CHARACTERSET'; SELECT value FROM v$nls_parameters WHERE parameter = 'NLS_NCHAR_CHARACTERSET';常见组合:数据库是 ZHS16GBK,ODAC 直接模式下要关掉 Unicode,或者把客户端的 NLS_LANG 设成SIMPLIFIED CHINESE_CHINA.ZHS16GBK;数据库是 AL32UTF8,ODAC 要开 UseUnicode。ODAC 的 NLS 开关集中在连接控件的 Options 和 Session 属性上:
OraSession1.Options.EnableNLS := True; OraSession1.UseUnicode := True; OraSession1.Session.NLSLanguage := 'SIMPLIFIED CHINESE';EnableNLS 开了之后,ODAC 会按 NLS 设置自动转换字符集,省去手工转换。但乱码问题不能只靠 ODAC,OCI 模式下客户端还有环境变量 NLS_LANG 要一起设。开发机正常、客户机乱码的场景,八成是客户机的 NLS_LANG 环境变量缺失。直接模式可以避开这个变量依赖,这也是直接模式在项目实施里最大的价值。
4.4 Delphi 编译/运行内存错误:包版本与线程现场
现象:Delphi 编译工程时偶发内存错误,或程序一启动就 Access Violation;更隐蔽的是在 TOraQuery.Open 附近随机崩溃。
原因有两个高频点:第一,ODAC 设计时包与当前 Delphi 版本不符,IDE 加载 BPL 时布局冲突;第二,TOraSession 被多个线程同时使用,ODAC 的会话并非线程安全,多线程查询必须每个线程建独立会话。
包版本不匹配的解决方式:完全卸载 ODAC,重新安装对应 Delphi 版本的包;如果自己从源码编译,务必确认条件编译符号与编译器版本一致。我见过最典型的翻车是 RAD Studio 11 工程里用了 10.4 编译的 ODAC 包,IDE 不报错,运行到连接初始化时崩溃,折腾一晚上后重装包解决。
多线程访问的问题,建议用线程独立会话模式:
LSess := TOraSession.Create(nil); try LSess.Options.Net := nnDirect; LSess.ConnectString := FBaseConnectString; LSess.Connect; { 执行查询 } finally LSess.Free; end;这段代码放在线程执行体里,每个线程创建自己的会话和查询组件,线程结束统一释放。共享同一个 TOraSession 做并发查询这种省事方案,最终都会在内存错误上踩回来。线程里要传连接串,用基类字符串字段复制,不要直接引用主线程的组件实例。
5. 发布与部署:OCI DLL 清单、连接配置外置与运行时验证
5.1 OCI 客户端 DLL 清单与直接模式免部署
发布 Delphi + ODAC 程序,最怕客户现场少文件。直接模式下 ODAC 自带网络协议实现,理论上只需要 Delphi 运行时库和 ODAC 运行时 BPL;若采用静态包或把关键单元编译进 exe,连 BPL 都不需要带。OCI 模式则需要 Oracle 客户端或者 Instant Client 的 DLL,缺少的首选是 oci.dll,此外还可能有 orawtc 系列、oraocci 系列和 annafs 字符集文件。判断需要哪些文件,可以在开发机用进程监视工具看程序启动时加载的 Oracle DLL,然后按清单拷贝。
更稳妥的部署策略是直接模式优先:在配置文件里把默认连接写成 Direct=True,用户无感;只有用到 OCI 专有功能时,才把 Instant Client 目录放在程序同级的 client 文件夹,并在启动代码里动态设置 Options.HomeName 指向该目录。这样发布包可以做成两个体积档位,普通环境用轻量档,重功能环境用完整档。
5.2 连接参数外置:INI 文件与配置读取
把连接串写死在代码里,换环境就要重新编译,这是老项目的通病。我一般在程序同级放一个 config.ini,保存服务器、端口、SID、用户名、密码和 Direct 标记。读取代码用系统自带 TIniFile:
uses IniFiles, SysUtils; procedure TDataModule1.LoadConfig(const AFileName: string); var LIni: TIniFile; begin LIni := TIniFile.Create(AFileName); try FOraSession.ConnectString := LIni.ReadString('DB', 'ConnectString', 'Server=127.0.0.1:1521:ORCL;SID=ORCL;Direct=True;'); FOraSession.UserName := LIni.ReadString('DB', 'User', 'scott'); FOraSession.Password := LIni.ReadString('DB', 'Password', 'tiger'); if SameText(LIni.ReadString('DB', 'NetMode', 'Direct'), 'OCI') then FOraSession.Options.Net := nnOCI else FOraSession.Options.Net := nnDirect; finally LIni.Free; end; end;这里用字符串读 NetMode,比用整数可靠,因为枚举值和 ODAC 版本没有稳定对应关系。密码明文放 INI 有安全隐患,面对等保或金融客户,建议把密码字段做简单混淆,再配合系统权限设置文件访问。重点是把改连接不影响编译这个能力交到实施人员手里。
5.3 运行时验证:到底走的是直接模式还是 OCI
部署完成后,第一个要验证的是连接模式。我见过客户把 Direct=True 的配置改成 OCI,现场没有 Oracle 客户端,程序直接报找不到 oci.dll。代码里加一个状态栏输出,能快速确认:
case FOraSession.Options.Net of nnDirect: StatusBar1.Panels[0].Text := 'Net: Direct'; nnOCI: StatusBar1.Panels[0].Text := 'Net: OCI'; end;甚至可以在连接成功后读取 FOraSession.ServerVersion,显示在关于对话框里。再深一层,从数据库端验证连接来源:
SELECT machine, program, username, status FROM v$session WHERE username = 'SCOTT' ORDER BY logon_time DESC;如果 program 显示的是你的 exe 名称,说明连接确实从该程序发出;如果 machine 异常,检查 hosts 解析。这个验证动作建议写进项目验收清单,尤其是在客户网络环境复杂的项目中,它能帮我们快速分界是代码问题还是环境问题。ODAC 的部署坑往往不在函数调用,而在发布清单的完整性上,直接模式打包体积小、独立性强;OCI 模式必须把客户端目录和程序目录一起制作成发布包,并在实施文档里写明版本匹配关系。
6. 连接池与故障重连:ODAC Options 里值得打开的三个开关
6.1 连接池:Pooling 参数与最大连接数
ODAC 包提供连接池机制。第一种方式直接在 TOraSession 上打开 Pooling,适合单 DataModule 的小应用;第二种用独立的 TOraConnectionPool 组件集中管理,适合多窗体的中型系统。核心参数是最大连接数、最小连接数和超时:
FOraSession.Pooling := True; FOraSession.PoolingOptions.MaximumNumberOfConnections := 30; FOraSession.PoolingOptions.MinimumNumberOfConnections := 2;把 MaximumNumberOfConnections 设太大,对数据库端 processes 参数是压力,设置前先确认数据库余量。Minimum 设 2 的意义是避免连接完全冷启动,但应用启动阶段会多两个空闲连接,看现场接受度。
6.2 本地故障转移:LocalFailover 与重试
ODAC 的 Options.LocalFailover 值得打开。当 Oracle 连接断开,ODAC 按设定的重试次数和间隔自动重连,重连后尝试重放 SQL。对客户端来说,数据库重启或者网络瞬断期间应用不崩溃。
FOraSession.Options.LocalFailover := True; FOraSession.Options.FailoverSettings.RetryCount := 3; FOraSession.Options.FailoverSettings.RetryDelay := 1000;注意,SQL 重放只对部分语句可靠,长事务里重放可能产生不一致,所以业务层还要设计幂等。这个开关的定位是应急,不是替代事务设计。
6.3 会话监控与验证:从 v$session 看连接状态
连接池是否真的在复用连接,从数据库端看最直接。打开连接池后,连续执行两次查询,v$session 里应该看到同一个 SID 在不同时间出现,而不是每次都新建。如下图所示意图的查询可以辅助判断:
SELECT sid, serial#, username, machine, program, logon_time, status FROM v$session WHERE username = 'SCOTT' ORDER BY logon_time DESC;多执行几次,观察新 SID 的数量。如果每次操作都冒出新 SID,说明连接池没有生效,回查 Pooling 是否在连接建立之前设置。连接池设置必须在 Connect 前完成,这一点容易漏。还有一次踩坑经历:用 TOraConnectionPool 分配会话后,代码里又手动改了连接串属性,池里的会话状态被污染,之后拿到的都是旧连接,从那以后我每次接手新项目都会先问一句:业务里有没有对象类型、高级队列、分布式事务?有就 OCI,没有就 Direct,并把这条结论写进技术选型文档。希望帮到你。
本文还有配套的精品资源,点击获取