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,如下圖: