1. 引言
1.1. 背景
本文延续《OCCI开发环境的安装和配置》一文,目的主要是搭建一个可用的OCCI开发环境。作为环境的验证,本文给出一个小例子,获取Oracle数据库的系统时间的小程序,在OCCI环境开发和运行。
同时,总结一下在测试过程中遇到的所有问题和使用的知识,不仅限于OCCI,包括IDE、编译器、操作系统(Linux)。
1.2. 系统环境
数据库:Oracle12c
客户端:instantclient_12_2
操作系统:Ubuntu16.04.3 Linux kernel 4.4.0-112-generic
IDE:Eclipse Oxygen.2 Release (4.7.2) Build id: 20171218-0600
编译器:gcc 4.8.5
2. 程序代码
2.1. 主程序
main.cpp
1 #include <string> 2 #include <iostream> 3 #include "CMyDatabase.h" 4 5 Int main (int argc, char * argv[]) 6 7 { 8 9 CMyDatabase stdb; 10 std::string user = "system"; 11 std::string passwd = "Oracle123"; 12 std::string connStr = "storcdb"; 13 14 stdb.connect (user, passwd, connStr); 15 stdb.getSystemDateFromDatabase (); 16 17 return 0; 18 19 }
主程序很简单。CMyDatabase类是我封装的Oracle数据库访问用的类,在主程序中,我们只要知道我们通过这个类连接数据库(connect),并且可以获得数据库系统的时间(getSystemDateFromDatabase)。
在连接数据库时,我们需要提供数据库访问的用户名和密码,以及标识数据库的网络服务名称或者说数据库连接字符串。可以参考《OCCI开发环境的安装和配置》一文中的4.1节以及4.2节末尾使用sqlplus测试数据库连接的部分内容。
2.2. CMyDatabase
CMyDatabase类主要封装了Oracle的Environment和Connection类,由于这两个类(OCCI中其他类如Statement类也存在这种情况)的创建都是以指针的方式返回,需要进行销毁,所以,将这种资源类封装在管理类里面,本例为CMyDatabase,由CMyDatabase负责创建和销毁Environment和Connection类的实例,从而保证资源的正确使用,即创建和销毁。
CMyDatabase.h
1 #ifndef CMYDATABASE_H_ 2 #define CMYDATABASE_H_ 3 4 #include <string> 5 #include <iostream> 6 #include <occi.h> 7 using namespace oracle::occi; 8 9 class CMyDatabase 10 { 11 public: 12 CMyDatabase() : m_env(NULL), m_conn(NULL) 13 { 14 } 15 virtual ~CMyDatabase(); 16 void connect(const std::string & user, const std::string & passwd, 17 const std::string & connStr); 18 void getSystemDateFromDatabase(); 19 private: 20 Environment * m_env; 21 Connection * m_conn; 22 }; 23 24 #endif /* CMYDATABASE_H_ */
CMyDatabase.cpp
1 #include "CMyDatabase.h" 2 3 CMyDatabase::~CMyDatabase() 4 { 5 if (NULL != m_conn) 6 { 7 m_env->terminateConnection(m_conn); 8 m_conn = NULL; 9 } 10 if (NULL != m_env) 11 Environment::terminateEnvironment(m_env); 12 } 13 14 void CMyDatabase::connect(const std::string & user, const std::string & passwd, 15 const std::string & connStr) 16 { 17 try 18 { 19 m_env = Environment::createEnvironment(); 20 m_conn = m_env->createConnection(user, passwd, connStr); 21 } 22 catch (SQLException & e) 23 { 24 std::cout << "using " << user << "/" << passwd << "@" << connStr 25 << " connect to database." << std::endl; 26 std::cout << "*** " << e.getErrorCode() << ": " << e.getMessage() 27 << std::endl; 28 } 29 } 30 31 void CMyDatabase::getSystemDateFromDatabase() 32 { 33 Statement * stmt = m_conn->createStatement(); 34 ResultSet * rs = stmt->executeQuery( 35 "select to_char(sysdate, 'YYYY-MM-DD HH:MI:SS') from dual"); 36 rs->next(); 37 std::cout << rs->getString(1) << std::endl; 38 stmt->closeResultSet(rs); 39 m_conn->terminateStatement(stmt); 40 }
在void CMyDatabase::getSystemDateFromDatabase();函数中创建一个Statement对象,并执行一个获得数据库系统时间的SQL语句,执行之后会返回一个ResultSet(结果集),访问结果集之前需要调用next()函数,实际上只有该函数返回Status::DATA_AVAILABLE(如果是流类型的数据,返回值为Statue:: STREAM_DATA_AVAILABLE)的时候才表明有数据,可以获得结果集中的数据。另外,对于数据库函数的使用要将其放入异常捕获代码块中,就像CMyDatabase::connect函数中写的一样。在这里我省略的必要的判断,在正式的代码中一定不要忘记。
3. 问题分析
本文不打算讲解使用Eclipse创建这个C++项目的过程,我相信有很多资料会讲,而且,我相信你是一个有经验的人,即使你是初学者,我认为通过摸索你也能够很快地把项目正确地创建出来。我们都是程序员,有这智商。
因此,这一章主要讲解一下这个例子中所遇到的一些问题,希望对大家有所帮助。
3.1. 安装Ubuntu16.04
如果你需要安装一个全新的Ubuntu16.04,我建议你直接到官方网站下载的最新版本的Ubuntu16.04.3(ubuntu-16.04.3-desktop-amd64.iso),当然,也可以尝试更高的版本。在最初的16.04安装包中存在问题,执行系统更新(sudo apt-get update)的时候会崩溃,需要将libappstream3包清除掉(sudo apt-get purge libappstream3)或者手工下载libappstream包进行安装,解决该问题。
3.2. 为什么是g++-4.8
当你安装了Ubuntu16.04或者更高的版本时,你的系统默认或者执行sudo apt-get install g++之后所安装的g++版本均在5.0以上。由于OCCI库是在gcc-4下编译,在gcc-5(5以上版本未测试)上编译后,程序执行会崩溃。具体的原因我目前还不清楚,推测与两个版本的std::string实现有关。
基于以上原因,我们需要为系统安装g++-4.8:
sudo apt-get install g++-4.8
如果是经过g++ 5.4编译过的程序,在连接数据库时会收到ORA-24960异常,并且程序崩溃转储。具体异常错误如下:
ORA-24960: the attribute OCI_ATTR_USERNAME is greater than the maximum allowable length of 255 ...... |
如果使用makefile来编译,那么制定编译和链接工具为g++-4.8就可以了;如果使用Eclipse编译,同样也需要配置编译和链接工具为g++-4.8。在项目上点击右键选择Properties --> C/C++ Build --> Settings,修改如下图标记的位置为g++-4.8。
3.3. 'std::string' is ambiguous '
当在系统中安装了g++-4.8之后,由于有多个gcc版本的存在,Eclipse会找到多个gcc版本的头文件,所以,Eclipse会对你用到的类型提示ambiguous,例如std::string。
当右键点击查看定义时,会弹出选择具体头文件的窗口,如下图所示。
如同配置g++-4.8一样,在Eclipse项目上鼠标右键点击打开Properties --> C/C++ Build --> Settings,按照下图示例设置“模棱两可”的头文件引用。
对于这个“include files”设置,除了解决头文件选择的二义性之外,是否还有其他什么作用?
4. 知识延伸
4.1. SONAME
在前一篇文章《OCCI开发环境的安装和配置》中提到动态库libclntshcore.so.12.1是否需要建立没有版本号的符号链接这个问题。通过对动态库的SONAME进行分析可以得到答案。
首先,我们来看使用到libcclntshcore.so.12.1动态库的libclntsh.so.12.1的ELF动态库引用信息,如下图:
readelf -d /opt/oracle/instantclient_12_2/libclntsh.so Dynamic section at offset 0x3859bc0 contains 35 entries: 标记 类型 名称/值 0x0000000000000001 (NEEDED) 共享库:[libmql1.so] 0x0000000000000001 (NEEDED) 共享库:[libipc1.so] 0x0000000000000001 (NEEDED) 共享库:[libnnz12.so] 0x0000000000000001 (NEEDED) 共享库:[libons.so] 0x0000000000000001 (NEEDED) 共享库:[libdl.so.2] 0x0000000000000001 (NEEDED) 共享库:[libm.so.6] 0x0000000000000001 (NEEDED) 共享库:[libpthread.so.0] 0x0000000000000001 (NEEDED) 共享库:[libnsl.so.1] 0x0000000000000001 (NEEDED) 共享库:[librt.so.1] 0x0000000000000001 (NEEDED) 共享库:[libaio.so.1] 0x0000000000000001 (NEEDED) 共享库:[libresolv.so.2] 0x0000000000000001 (NEEDED) 共享库:[libc.so.6] 0x0000000000000001 (NEEDED) 共享库:[ld-linux-x86-64.so.2] 0x0000000000000001 (NEEDED) 共享库:[libclntshcore.so.12.1] 0x0000000000000001 (NEEDED) 共享库:[libgcc_s.so.1] 0x000000000000000e (SONAME) Library soname: [libclntsh.so.12.1]
我们看到动态库libclntsh.so.12.1引用的libclntshcore.so.12.1动态库,动态库在加载的时候,是根据引用的名字来寻找和加载动态库的。所以,只要在动态库的路径中,存在这个libclntshcore.so.12.1文件就完全没有问题。
那什么时候我们需要没有版本好的动态库符号链接呢?作为程序员,我们开发程序的过程中,很多情况下都需要链接其他动态库文件,我们使用-l选项链接程序时,编译器在链接过程中对-l选项的参数进行处理,比如:-lcuda,那么,编译器在链接过程中,在cuda前后分别加上lib和.so,即为我们寻找libcuda.so文件,这种处理方式,使我们在链接环境下不得不为具体的带有版本号的动态库文件创建没有版本号的符号链接文件,从而,使我们开发过程中编译链接时能够找到所引用的动态库。
从上面的图中我们看到了三个有关cuda的动态库文件,而实质上只有一个“真正”的动态库文件。那么,为什么要多一个libcuda.so.1呢?我们使用readelf命令查看一下libcuda.so.387.26的动态库信息:
注意,看SONAME一行,library name 是libcuda.so.1。虽然,我们在链接我们的程序时,使用的是无版本号的符号链接,但是,在我们编译好的程序所依赖的动态库,是以SONAME为依据的,也就是说,我们会依赖libcuda.so.1动态库文件。所以,我们的程序在执行过程中,操作系统帮我们加载动态库时会根据我们依赖的动态库名字去搜索,所以,操作系统会搜索libcuda.so.1动态库文件。
为什么要这样呢?
1、依赖于有版本号的动态库加载可以实现不同版本的动态库共存;
2、不使用完整版本号,仅使用大版本号,可以使动态库升级小版本时不影响依赖该动态库的具体应用程序。
很周到!
回过头来,我们用readelf查看一下libcclntshcore.so.12.1动态库的SONAME为“libclntshcore.so.12.1”,如下图:
readelf -d /opt/oracle/instantclient_12_2/libclntshcore.so
Dynamic section at offset 0x3b8f80 contains 30 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libdl.so.2]
0x0000000000000001 (NEEDED) 共享库:[libm.so.6]
0x0000000000000001 (NEEDED) 共享库:[libpthread.so.0]
0x0000000000000001 (NEEDED) 共享库:[libnsl.so.1]
0x0000000000000001 (NEEDED) 共享库:[librt.so.1]
0x0000000000000001 (NEEDED) 共享库:[libaio.so.1]
0x0000000000000001 (NEEDED) 共享库:[libresolv.so.2]
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x0000000000000001 (NEEDED) 共享库:[ld-linux-x86-64.so.2]
0x0000000000000001 (NEEDED) 共享库:[libgcc_s.so.1]
0x000000000000000e (SONAME) Library soname: [libclntshcore.so.12.1]
……
文件名与SONAME一致,我们开发又不需要直接引用该动态库,所以,无需为该动态库创建符号链接。
在eclipse中,我们可以通过项目属性来设置动态库的SONAME,如下图: