Author: Ma, Hongbin
概要
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的一系列赋值操作(