68、C语言与Oracle基础:OCI/OCCI、Oracle环境初始化、SQL执行、结果集获取
说实话,很多做嵌入式或者C语言开发的朋友,一听到要跟Oracle数据库打交道,心里就发怵。觉得那是Java或者C#的专属领域。其实不然。Oracle提供了两套C语言的接口:OCI和OCCI。我最早接触OCI是在一个工业数据采集项目里,当时甲方指定要用Oracle,我硬着头皮啃了半个月的文档。后来发现,只要把环境初始化、SQL执行、结果集获取这三步走通,后面就顺了。
OCI与OCCI:选哪个?
先说说这两个东西的区别。OCI是Oracle Call Interface,纯C接口,函数式编程。OCCI是Oracle C++ Call Interface,面向对象的C++封装。我个人习惯用OCI,原因很简单:嵌入式环境里C更通用,而且OCI的底层控制力更强。你想想看,在资源受限的设备上,少一层封装就少一份不确定性。
| 特性 | OCI | OCCI |
|---|---|---|
| 语言 | C | C++ |
| 编程范式 | 过程式 | 面向对象 |
| 学习曲线 | 较陡 | 相对平缓 |
| 资源占用 | 低 | 中等 |
| 适用场景 | 嵌入式、高性能 | 快速开发、C++项目 |
不过话说回来,如果你团队里都是C++程序员,用OCCI也没毛病。我见过不少项目用OCCI写业务逻辑,效率也挺高。关键看你的项目背景。
Oracle环境初始化:第一步不能错
OCI编程的第一步,就是初始化环境。这一步要是搞错了,后面全白搭。我记得第一次写OCI程序时,忘了调用OCIEnvCreate,结果程序一跑就段错误,查了半天才发现是环境句柄没创建。
标准流程是这样的:
- 创建环境句柄(OCIEnvCreate)
- 分配错误句柄(OCIHandleAlloc)
- 分配服务上下文句柄
- 分配会话句柄
- 连接数据库
来看一段代码:
#include <oci.h>
OCIEnv *envhp;
OCIError *errhp;
OCISvcCtx *svchp;
OCISession *authp;
// 1. 创建OCI环境
OCIEnvCreate(&envhp, OCI_DEFAULT, NULL, NULL, NULL, NULL, 0, NULL);
// 2. 分配错误句柄
OCIHandleAlloc((dvoid *)envhp, (dvoid **)&errhp, OCI_HTYPE_ERROR, 0, NULL);
// 3. 分配服务上下文
OCIHandleAlloc((dvoid *)envhp, (dvoid **)&svchp, OCI_HTYPE_SVCCTX, 0, NULL);
// 4. 分配会话句柄
OCIHandleAlloc((dvoid *)envhp, (dvoid **)&authp, OCI_HTYPE_SESSION, 0, NULL);
// 5. 设置用户名密码
OCIAttrSet(authp, OCI_HTYPE_SESSION, "scott", 5, OCI_ATTR_USERNAME, errhp);
OCIAttrSet(authp, OCI_HTYPE_SESSION, "tiger", 5, OCI_ATTR_PASSWORD, errhp);
// 6. 设置服务上下文
OCIAttrSet(svchp, OCI_HTYPE_SVCCTX, authp, 0, OCI_ATTR_SESSION, errhp);
// 7. 开始会话
OCISessionBegin(svchp, errhp, authp, OCI_CRED_RDBMS, OCI_DEFAULT);
SQL执行:准备、绑定、执行三步走
环境初始化好了,接下来就是执行SQL。OCI执行SQL的流程比MySQL的C API要复杂一些,但逻辑更清晰。说白了就是三步:准备语句、绑定变量、执行。
我习惯用OCIStmtPrepare准备SQL语句,然后用OCIBindByPos或OCIBindByName绑定参数。这里有个坑:绑定变量的类型必须和数据库字段类型匹配,否则会报ORA-01438之类的错误。
OCIStmt *stmthp;
OCIHandleAlloc(envhp, (dvoid **)&stmthp, OCI_HTYPE_STMT, 0, NULL);
// 准备SQL
text *sql = (text *)"SELECT empno, ename, sal FROM emp WHERE deptno = :1";
OCIStmtPrepare(stmthp, errhp, sql, strlen((char *)sql), OCI_NTV_SYNTAX, OCI_DEFAULT);
// 绑定输入参数
int deptno = 10;
OCIBindByPos(stmthp, &bindhp, errhp, 1, &deptno, sizeof(deptno), SQLT_INT, NULL, NULL, NULL, 0, NULL, OCI_DEFAULT);
// 执行
OCIStmtExecute(svchp, stmthp, errhp, 1, 0, NULL, NULL, OCI_DEFAULT);
OCIStmtExecute的iters参数控制执行次数。比如批量插入时,设置iters为100,一次调用就能插入100条记录,性能提升很明显。
结果集获取:一步步取数据
SQL执行完了,怎么拿数据?OCI用的是OCIDefineByPos来定义输出变量,然后循环调用OCIStmtFetch2取数据。这个过程有点像从井里打水——你得先准备好桶(定义变量),然后一桶一桶地提(fetch)。
// 定义输出变量
int empno;
char ename[20];
double sal;
OCIDefineByPos(stmthp, &defhp, errhp, 1, &empno, sizeof(empno), SQLT_INT, NULL, NULL, NULL, OCI_DEFAULT);
OCIDefineByPos(stmthp, &defhp, errhp, 2, ename, sizeof(ename), SQLT_STR, NULL, NULL, NULL, OCI_DEFAULT);
OCIDefineByPos(stmthp, &defhp, errhp, 3, &sal, sizeof(sal), SQLT_FLT, NULL, NULL, NULL, OCI_DEFAULT);
// 循环取数据
while (OCIStmtFetch2(stmthp, errhp, 1, OCI_FETCH_NEXT, 0, OCI_DEFAULT) == OCI_SUCCESS) {
printf("Empno: %d, Name: %s, Sal: %.2f\n", empno, ename, sal);
}
这里有个细节:OCIStmtFetch2的第三个参数是行数,设为1就是一次取一行。如果你内存够用,也可以一次取多行,比如设为100,然后通过OCIDefineArrayOfStruct来接收。我在一个报表导出项目里用过这个技巧,速度提升了将近10倍。
知识体系总览
下面这张图是我整理的OCI编程核心流程,你可以对照着看:
避坑指南
做OCI开发,有几个坑我踩过,分享给你:
- 句柄泄漏:每次
OCIHandleAlloc都要对应一个OCIHandleFree。我曾经在一个循环里忘了释放语句句柄,跑了三天后数据库连接池爆了。 - 字符集问题:OCI默认用数据库的字符集。如果你传的是UTF-8字符串,但数据库是GBK,会出现乱码。建议在
OCIEnvCreate时指定OCI_UTF8。 - 事务管理:OCI默认是自动提交模式。如果你需要事务控制,记得在
OCIStmtExecute时设置OCI_DEFAULT,然后手动调用OCITransCommit或OCITransRollback。
我的经验:刚开始学OCI时,别急着写复杂逻辑。先把环境初始化、SQL执行、结果集获取这三个基础流程跑通。我当年就是先写了一个"select 1 from dual"的测试程序,确认环境没问题后,再逐步加业务逻辑。这样出了问题也好排查。
嗯,OCI的内容其实挺多的,但核心就是这三板斧。你把这套流程吃透了,后面什么存储过程调用、LOB操作、数组绑定,都是锦上添花的事。记住,数据库编程最怕的就是基础没打牢就往上堆功能。