熱線電話:13121318867

登錄
首頁精彩閱讀如何定制一個基于REST Service的ODBC驅動程序_數據分析師
如何定制一個基于REST Service的ODBC驅動程序_數據分析師
2014-12-01
收藏


如何定制一個基于REST Service的ODBC驅動程序



REST Service能夠幫助開發者以簡單統一的接口向終端用戶提供服務。然而數據分析的應用場景中,一些成熟的數據分析工具(例如Tableau, Excel等)要求用戶提供ODBC數據源,在這種情況下,REST Service并不能滿足用戶所有對數據的使用需求。本文從實現的角度詳細介紹了如何在現有REST Service的基礎上,完成一個定制ODBC驅動程序的開發。文章側重介紹了ODBC驅動程序的實現原理,結合代碼詳細說明了ODBC與REST Service之間的數據交互,并在文章末尾介紹了ODBC客戶端程序調用ODBC API的原理,以及實際開發中調試環境的搭建。

  可能受益的讀者

  目前主流的數據分析工具,例如Tableau,Microstrategy,excel都只能夠ODBC Driver,來訪問底層的數據源。也就是說,在開發數據庫或者數據倉庫的過程中,即使我們已經實現了符合SQL規范的數據訪問接口,哪怕提供了自己的JDBC驅動程序,仍然無法保證數據用戶能夠有效地使用我們的數據。為此,我們需要額外地為數據源定制一個ODBC Driver。

  如果你的數據源恰好是類似MongoDB,Hbase這樣的常見數據庫產品,你或許可以考慮直接從購買一些商業產品,例如Simba ODBC Driver來一勞永逸地解決你的需求,但是將意味著不小的開支。更難辦的情況是你的數據源并不是那么主流,還沒有任何可以直接購買的驅動程序可以適用于它,那么定制一個自己的ODBC Driver可能是你最好的選擇。即使你是一個對ODBC Driver一無所知的開發者,本文也將給你帶來或多或少的幫助。

  我們的處境

  簡單地說,我們團隊用java開發了一個特別的SQL引擎。在項目初期我們只有JDBC驅動程序,還有一個用于服務于網頁客戶端的REST Server,但是我們沒有ODBC 驅動,因此大多數的客戶并不能真正地使用我們的產品完成他們地工作。

  為了解決這個問題,我們設計了如下圖的解決方案:我們使用REST Server統一地接受來自所有客戶端的請求,包括網頁客戶端和使用ODBC Driver的客戶端。REST Server中使用JDBC驅動來訪問我們的數據庫。當然如果你的客戶端就是一個java程序,你完全可以直接通過JDBC來訪問我們的數據庫,從而節省這些步驟帶來的開銷。這張圖片中并未展示這種情況。

  在客戶端,我們深度定制了一個專有的ODBC Driver,它向上層的應用程序提供了標準的ODBC API,封裝所有實現的邏輯。在底層實現上,它調用C++的REST庫,將應用程序發送過來的SQL查詢請求封裝成REST請求,發送給我們的REST Server,并在得到結果后,再以符合ODBC規范的方式,返回給上層的應用程序。


 

  從Hello World開始

  對于從來沒有接觸過ODBC的開發者來說,了解一個ODBC客戶端的行為有助于理解定制一個ODBC驅動需要實現哪些具體的API。下圖中展示了一個簡單的ODBC客戶端程序的實現,每一行代碼都配有詳細的注釋解釋它的行為,通讀代碼,不難擁有一個直觀的理解。為了簡化代碼,我們省略了所有錯誤檢查的代碼。所有的SQLXXX格式的函數,都是ODBC定義的標準API。

  我們將這段程序分成了五塊區域,分別標記為A~E。A區域和B區域依次初始化了三個與ODBC相關的句柄,分別是:

  Environment handle (hEnv):包含一個或者多個Connection handle。同時,一些全局的信息也包含在內,例如客戶端所需要的ODBC版本,以及環境級別的診斷信息。

  Connection handle (hConn):代表了一個對DBMS/數據源的連接,包含了連接級別的信息,例如連接的超時時間,隔離級別,以及連接級別的診斷信息。

  Statement handle (hStmt):可以將它看做是某個具體的查詢請求,例如 SELECT * FROM employee。

  值得一提的是ODBC規范只定義了數據源以何種方式暴露數據訪問的接口,但是并沒有規定如何實現,這也包括三類句柄的具體實現。事實上,在代碼中這三類句柄都通過SQLHANDLE類型來傳遞,而SQLHANDLE本質上是一個void *類型,指向我們自定義的相應的結構體。

  ODBC為應用程序提供了一系列的C語言風格的API來支持訪問查詢。不同于面向對象語言的驅動程序,使用ODBC驅動程序的應用程序需要為將要返回的數據提前準備好內存區域,從這個角度說,ODBC的任務是正確地將用戶需要的數據,搬運到用戶指定的內存區域之中(可能帶有一些數據轉化,例如如果應用程序需要支持Unicode,那么ODBC Driver可能需要將char類型的源數據轉化為wchar類型)。下圖的A區域中初始化了一系列的句柄和變量,其中第305~307行就在程序的棧上開辟了這樣一些用作緩存的內存區域。事實上,在E區域,我們傳入了變量x和i的引用,因此我們可以把第308~309行的兩個數值變量也看作是這樣存儲返回結果的內存區域。


 

  在區域C中,我們調用SQLDriverConnect函數,同時傳入hConn句柄和連接數據源所需要的用戶名,密碼,驅動名稱等信息。我們在ODBC Driver的實現中,完成對hConn的一系列賦值操作(其初始化操作已經在B區域中完成),使得hConn成為一個可用的連接句柄。當SQLDriverConnect的返回值等于SQL_SUCCESS的時候,一個DBMS/數據源連接就正式被建立好了。

  在區域D中,客戶端程序首先對Statement句柄hStmt進行了初始化,然后直接使用SQLExecDirect API進行查詢。該API的第二個參數接收的字符串,即為查詢請求的SQL。

  在最后的E區域中,客戶端獲取查詢結果。在這段程序中,客戶端首先在第331行利用SQLColAttribute接口提取了第一列的列名屬性,其中第二個參數指定返回結果的第一類,第三個常量參數SQL_DESC_NAME指定所需要的屬性標志(列名)。接下來,客戶端利用SQLBindCol接口,告知ODBC Driver它希望將第一列的返回結果填入到szColData所指向這段內存中,并且用常量參數SQL_C_TCHAR告知ODBC客戶端希望看到的返回類型是char數據類型,這個過程被稱作綁定(Bind)。一切就緒之后,客戶端調用SQLFetch接口獲取結果第一行的第一列,由于沒有綁定其他的返回列,因此SQLFetch實際上只會返回第一列的內容。

  在一個更加現實的客戶端代碼中,可能會首先調用SQLNumResultCols接口得知返回結果總共有多少列。對于每一個返回的列,客戶端使用比SQLColAttribute接口更加便捷的SQLDescribeCol接口,一次性獲取該列的所有基本信息,包括列名,類型,長度等信息。根據返回的列信息,客戶端有針對性地調整SQLBindCol的參數,以便正確地接受相應的返回結果。一切就緒之后,客戶端調用SQLFetch接口,得到需要的查詢結果。由于每次調用SQLFetch返回結果集中的一行,客戶端程序需要重復調用SQLFetch,直到SQLFetch不再返回SQL_SUCCESS,而是返回SQL_NO_DATA,表示已經不再有更多的行可以返回??蛻舳丝梢愿鶕枨蟮牟煌?,考慮就究竟是復用同一塊內存空間來接受結果中不同的行(取一行,使用一行),還是在一開始就申請能夠容納所有行的大塊內存,每次綁定傳入不同的內存位置(取完所有行后再一起使用結果數據)。

  開始定制ODBC Driver

  通過上一節對ODBC Driver所需要提供的接口有一定了解后,如果我們需要從無到有地寫出一個完整的ODBC Driver,我們需要完成兩項工作:

  1. 實現客戶端所需要的所有API,MSDN給出了ODBC規范中每一個API的詳細定義(http://msdn.microsoft.com/en-us/library/ms714562(v=vs.85).aspx, 慶幸的是我們沒必要實現每一個接口,只需要根據客戶端的行為找到最小的必要集),在Windows中,我們將所有的API的實現打包成為一個可執行模塊,通常是一個dll文件。

  2. 讓程序客戶端程序能夠正確地找到我們的Driver,簡而言之,我們需要正確地將ODBC Driver安裝到客戶端程序運行的機器上。

  由于在實現復雜度上,第二步明顯低于第一步,另外對第二步的介紹也有助于我們能夠對ODBC Driver有整體性的理解。因此雖然第二步事實上依賴于第一步的完成,我們仍然優先介紹第二步的實現。在這里我們可以假設我們已經實現了所有必須的API,這些API的實現都被包裝在一個名為driver.dll的文件中。

  第一步:安裝ODBC Driver

  理解ODBC架構

  在ODBC架構中( http://msdn.microsoft.com/en-us/library/aa266933(v=vs.60).aspx),有四個關鍵的模塊,分別是:

  API:通過調用ODBC的接口來連接數據源,發送和接受數據,以及關閉連接。這里的API僅僅是接口,并沒有實現,具體的實現需要在Driver模塊中完成。

  Driver Manager:向應用程序提供諸如可用的數據源的信息,按需動態加載驅動程序,提供參數檢查等。

  Driver:處理ODBC的函數方法,管理應用程序和特定的DBMS/數據源之間的所有交互。如果有必要,Driver還會將標準SQL格式的請求語句轉為目標數據源的原生SQL格式。

  Data Source:由數據及其數據庫引擎組成。


 

  其中API和Driver Manager已經一般是操作系統自帶的。在Window上,我們可以通過安裝MDAC (Microsoft Data Access Components, http://www.microsoft.com/en-us/download/details.aspx?id=5793),來獲得所有所需的頭文件已經相關的工具資源。在Unix環境中,也有類似的UnixODBC。在下文中我們僅考慮Windows下的ODBC開發。在開發ODBC Driver的時候,底層的數據源一般也已經就緒。因此我們僅需要將ODBC Driver注冊到Driver Manager之中。

  注冊ODBC Driver

  Driver Manager通過注冊表得知所有可用的ODBC Driver的列表,以及它們各自的詳細信息。具體位置在(假設目標機器安裝了64位Windows):

  32位 驅動:

  HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\ODBC\ODBCINST.INI\ODBC Drivers

  64位 驅動:

  HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers

  以32位Windows為例,我們打開注冊表中的 ODBC Drivers鍵,可以看到系統中所有安裝的32位ODBC的驅動程序都在其中,我們將自己的ODBC起名為ebayODBCDriver,并在ODBC Driver中加入相應的一行:


 

  在得知ODBC Driver的名字之后,Driver Manager會在ODBC Driver的父親節點上,也就是ODBCINST.INI中尋找相應的ODBC Driver的詳細信息。必備的信息包括Driver屬性和Setup屬性,分別告訴ODBC Manager在哪里尋找Driver和Setup的可執行程序。其中Driver對應于我們將要實現的ODBC Driver,而Setup程序中包括了一些設置DSN的時候用到的API,根據通常的慣例,這部分的API也和ODBC Driver的API一同編譯在同一個dll文件中,因此我們看到在ebayODBCDriver下,Driver和Setup指向同一個dll文件。在此也可以定義一些其他的屬性,但這些都是可選的。


 

  一切就緒,我們就能夠在Control Panel--Administrator Tools--Data Sources(ODBC) 中為我們的ODBD Driver創建DSN了。對于32位的ODBC Driver而言,我們需要使用C:\Windows\SysWOW64\odbcad32.exe這個32位版本的Data Sources(ODBC)。值得一提的是在使用Data Sources(ODBC)創建DSN的過程中,我們使用到了上文提及的Setup程序中的接口,尤其是ConfigDSN接口。( http://msdn.microsoft.com/en-us/library/ms709275(v=vs.85).aspx )

  為了簡化這些安裝步驟,我們可以以Windows Installer的形式,包裝所有這些注冊ODBC Driver的邏輯,讓使用者可以簡單地通過安裝一個exe,完成ODBC Driver的安裝和注冊。


 

  第二步:實現ODBC API

  Descriptors

  在MSDN對ODBC架構的闡述中,ODBC Driver模塊的核心職能在于管理應用程序和特定的DBMS/數據源之間的所有交互。交互的載體在于數據,而有數據就意味著需要內存空間對其進行存儲。前文已經提到,ODBC Driver本身定位于一個數據的搬運工,它將應用程序的請求轉交給數據源,并且將數據源返回的數據結果逐行搬運給應用程序。在這個過程中ODBC Driver需要至少兩塊內存區域,或者簡稱buffer:一塊用來緩存從數據源返回的結果,另外一塊用來緩存移交給應用程序的結果。這兩塊buffer不僅包含數據本身,還包括對數據的描述。例如在返回給應用程序的數據中,ODBC Driver不僅需要維護列數據本身,還維護了該列數據的類型,長度等信息,在ODBC Driver和應用程序之間,這些數據和信息統稱為Application Row Buffer Descriptor(ARD)。相應地,數據源交給ODBC Driver的也不僅僅是數據本身,還包括對每一個返回的列的元信息描述,這部分信息和數據統稱為Implementation Row Buffer Descriptor(IRD)。


 

  事實上ARD中保存數據本身的內存區域,是由應用程序在調用SQLBindCol的時候傳入的,ARD并不負責這段內存的申請和釋放,而存放其他信息所需要的內存則由ODBC Driver負責維護。當應用程序調用例如SQLNumResultCols,SQLColAttribute,SQLDescribeCol等接口的時候,ODBC Driver找到ARD中相應的內容,返回給調用者;當應用程序調用SQLBindCol和SQLFetch的時候,ODBC Driver通過ARD得知該返回數據應該被存放的位置(指針),從IRD中讀取最新的一行數據,施加一些必要的據類型轉化,將其搬運到指定位置。

  與ARD,IRD對應的,ODBC標準還提供了另外兩種buffer, 分別是Application parameter descriptor (APD) 和Implementation parameter descriptor (IPD),用來處理動態查詢中的參數,本文中對這兩類buffer不做詳細介紹。這四類buffer構成了ODBC世界中的四個最主要的Descriptor。更多信息,讀者可以參考(http://msdn.microsoft.com/en-us/library/ms716262(v=vs.85).aspx )。

  在具體的實現中,ARD和IRD被定義為特殊的結構體(struct),存放與代表Statement的結構體GenODBCStmt(Generic ODBC Statement)之中。下圖展示了我們一個GenODBCStmt結構體的部分成員:首先是標識其類型的標簽(區分與代表Environment和代表Connection的結構體),然后是前文提到的四種不同用途的descriptor,接著是Statement級別的一些屬性信息,SQL語句等等。


 

  我們以ARD為例詳細分析,ARD的具體實現不受ODBC規范的約束,可以自由實現。在我們的實現中,我們用結構體GENODBCARD代表一個Statement的所對應的ARD。每個ARD包括了一些所有返回列共享的信息,又包含了不同返回列的不同的詳細信息,用更細力度的結構體GenODBCARDItem來代表。


 

  觀察GenODBCARDItem中的成員變量,很容易和ODBC API產生一一對應的關系。例如這里的DataConciseType對應與SQLColAttribute返回的列類型信息,而DataPtr成員則對應SQLBindCol所傳入的內存空間的指針??偠灾?,大多數ODBC API的實現,本質上就是對ARD和IRD的不同成員變量的訪問和修改。


 

  利用REST API訪問數據源

  ARD負責ODBC Driver與應用程序之間的交互,其初始化在應用程序調用SQLBindCol的過程中完成。而IRD負責ODBC Driver與數據源之間的交互,其初始化需要在和數據源進行數據交換的的過程中完成。

  我們首先定義REST請求的接口:

  std::unique_ptr restQuery(

  wchar_t* rawSql, char* serverAddr, char* username, char* passwd);

  SQLResponse類封裝一個SQL請求所有的返回內容,對于每個SQL查詢,REST Server 返回一個SQLResponse的實例。該實例中的columnMetas成員包含了每一個返回列的信息,而results成員則以字符串形式保存了返回結果的每一行。


 

  ODBC Driver將返回的SQLResponse實例交給該Statement的IRD,這樣IRD在事實上擁有了該SQL查詢所有的返回結果。當諸如SQLFetch的API被調用的時候,ODBC Driver只需要找到IRD中的的SQLResponse實例,對其中的信息進行解析,即可返回調用者需要的信息。


 

  其他實現

  SQLTables,SQLColumns的實現

  應用程序調用這兩個API來獲得當前連接所能查詢的所有表和列。由于這部分信息是整個數據庫連接過程中公用的,我們在應用程序SQLDriverConnect,建立連接句柄的時候,就向REST Server發送獲得所有表和列元信息的請求。類似于SQLResponse,我們用MetadataResponse封裝這些信息,并將該返回的實例交給代表連接句柄的GenODBCConn結構體負責維護。當SQLTable,SQLColumns的請求到來時,ODBC Driver從GenODBCConn的MetadataResponse中抽取需要的信息返回給調用者。

  SQLDriverConnect的實現

  檢查傳入的連接字符串,如果其中明確地給出了連接數據庫所需的地址,用戶名,密碼等信息,則直接用REST請求的方式確認REST Server存活,并且獲得表和列得元信息MetadataResponse。如果上述信息不完整,則向應用程程序彈出對話框補全這些信息。當然,調用者也可以直接在連接字符串中指定已經配置好的DSN,直接完成連接。

  SQLGetInfo的實現

  當應用程序無法通過ODBC Driver的名稱確認ODBC Driver的來源時,它將調用一系列的SQLGetInfo接口來獲得該ODBC Driver的一些特性,例如版本,支持的函數,支持的數據類型等。Tableau等BI工具會根據這些返回結果來選擇不同的行為,例如Tableau在生成SQL查詢的時候無法確定表名等標志符該用單引號還是雙引號來包圍,它就會調用SQLGetInfo來獲得SQL_IDENTIFIER_QUOTE_CHAR 屬性來確定。ODBC Driver實現的過程中需要通過對應用程序行為的分析,確定SQLGetInfo該返回的結果。

  Unicode的支持

  從ODBC 3.5開始,ODBC同時支持UNICODE和ANSI編碼的API。ODBC使用后綴W來代表支持UNICODE的接口,例如SQLDriverConnect和SQLDriverConnectW。詳情可以參考(http://msdn.microsoft.com/en-us/library/ms716246(v=vs.85).aspx )。

  客戶端原理及診斷

  客戶端原理

  在Visual Studio中創建一個標準的ODBC客戶端程序需要:

  引入頭文件,對該文件的引用會間接引用, , 等頭文件,在這些頭文件中包含了ODBC標準中的各種常量和方法的聲明,例如SQLDriverConnect方法和SQL_SUCCESS, SQL_ERROR常量等。

  在項目的Linker中,加入對odbc32.lib和odbccpp32.lib的依賴。在Visual Studio 2012中,新創建的C++項目默認就對這兩個靜態鏈接庫依賴,如下圖所示:


 

  這兩部所需的頭文件資源和靜態鏈接庫資源都由ODBC Manager(或者說Windows操作系統)提供。在這兩步完成之后,我們就可以像在文章開始的客戶端示例程序中那樣,任意地調用ODBC的API??蛻舳顺绦蚰軌虺晒Φ卦L問對應的ODBC驅動程序中的相應的API,歸功于ODBC Manager在中間的幫助。我們可以注意到在compile和link期間,客戶端程序并不與我們的ODBC驅動程序產生任何依賴,相反,客戶端程序只依賴于ODBC Manager提供的頭文件和靜態鏈接庫。其中的調用原理如下圖紫色箭頭所示:


 

  ODBC Manager在odbc32.lib和odbccpp32.lib中給出了所有ODBC API的“實現”,這樣客戶端才能在不依賴于任何具體ODBC Driver的前提下完成link。然而這種“實現”事實上只是一種單純的轉發,ODBC Manger根據客戶端程序中指定的驅動程序名稱,將客戶端的ODBC API請求轉發到相應的ODBC驅動程序中。

  以客戶端調用SQLDriverConnect 為例,由于客戶端程序和odbc32.lib被link在了一起,因此程序會首先進入odbc32.lib中對SQLDriverConnect的實現之中,該實現通過explicit linking(可以理解為運行時動態的link,參考 http://msdn.microsoft.com/en-us/library/784bt7z7.aspx),可以找到并調用我們真實的ODBC驅動程序的DLL(簡稱driver.dll)中的SQLDriverConnect方法,于是最終客戶端程序調用到了driver.dll中的SQLDriverConnect方法。

  ODBC客戶端程序,ODBC Manager,ODBC Driver這三者這樣的角色分配可以優雅地完成客戶端程序和具體驅動程序之間的解耦,保證了我們可以在系統中自由地增加和指定新的ODBC驅動程序。然而這樣的設計也給開發和調試帶來了麻煩:我們無法簡單地在Visual Studio中以Debug模式追溯客戶端調用過程中程序運行的每一步,因為客戶端并不對我們的驅動程序產生顯示的依賴,所以Visual Studio無法為我們找到驅動程序對應的源代碼。

  診斷客戶端程序

  解決方法是取消Visual Studio中的C++項目對ODBC Manger提供的靜態鏈接庫的依賴,轉而直接依賴自定制ODBC驅動程序的靜態鏈接庫(簡稱driver.lib,該靜態鏈接庫會在我們編譯driver.dll的時候作為副產品同時產生)。在Property Pages->Configuration Properties->Linker->Additional Dependencies中,我們首先取消”Inherit from parent or project defaults”選項,然后將”Inherited Values”中除了odbc32.lib和odbccpp32.lib以外的值拷入Additional Dependencies。這樣保證了我們不失去其它的項目默認依賴,同時也去除了客戶端對ODBC Manager的odbc32.lib和odbccpp32.lib的依賴。最后,我們在此加入driver.lib的路徑,使客戶端程序顯示地依賴我們的ODBC驅動程序的實現。


 

  值得一提的是我們仍然需要引用頭文件,在Visual Studio看來,唯一的不同之處是,頭文件中的ODBC API的實現不再在odbc32.lib和odbccpp32.lib之中,而是在我們的驅動程序driver.lib之中了。Visual Studio可以根據這種認識自動定位到驅動程序的源代碼,于是我們就可以在Visual Studio的Debug模式中自由地追溯程序間的函數調用了。

  其他Debug 工具

  ODBC Driver的Debug相對繁瑣,除了將一些關鍵步驟打印到日志文件,這里還推薦另外兩種方式進行補充:

  1. 使用ODBC Data Source Administrator的Tracing功能捕捉ODBC Manager的日志。


 

  2. Include windows.h, 使用OutputDebugString方法輸出debug語句,配合dbmon觀察程序輸出。該方式可以在log日志無法工作的時候作為補充,而且在dbmon沒有開啟的情況下,不會對系統造成額外的開銷。具體參見http://msdn.microsoft.com/en-us/library/windows/desktop/aa363362(v=vs.85).aspx 。

  總結

  本文詳細描述了如何在已有REST服務的前提下,完成一個以REST API作為后臺數據訪問方式的ODBC驅動程序。本文工作能夠幫助REST服務的提供團隊以ODBC的形式包裝其服務,使得他們的用戶能夠在更多的商用平臺上消費他們的服務。CDA數據分析師培訓官網


數據分析咨詢請掃描二維碼

若不方便掃碼,搜微信號:CDAshujufenxi

數據分析師資訊
更多

OK
客服在線
立即咨詢
日韩人妻系列无码专区视频,先锋高清无码,无码免费视欧非,国精产品一区一区三区无码
客服在線
立即咨詢