⌘+k ctrl+k
1.1.3 (stable)
Search Shortcut cmd + k | ctrl + k
ODBC 101: A Duck Themed Guide to ODBC

什么是ODBC?

ODBC 代表开放数据库连接,是一个允许不同程序与不同数据库通信的标准,当然也包括 DuckDB 🦆。这使得构建能够与许多不同数据库一起工作的程序变得更加容易,从而节省了时间,因为开发人员不必编写自定义代码来连接到每个数据库。相反,他们可以使用标准化的ODBC接口,这减少了开发时间和成本,并且程序更易于维护。然而,ODBC可能比其他连接数据库的方法(例如使用原生驱动程序)慢,因为它在应用程序和数据库之间增加了一层额外的抽象。此外,由于DuckDB是基于列的,而ODBC是基于行的,因此在使用ODBC与DuckDB时可能会出现一些效率低下的情况。

本页面中有多个链接指向官方的Microsoft ODBC 文档,这是学习更多关于 ODBC 的绝佳资源。

通用概念

Handles

一个句柄是指向特定ODBC对象的指针,用于与数据库进行交互。有几种不同类型的句柄,每种都有不同的用途,这些是环境句柄、连接句柄、语句句柄和描述符句柄。句柄是使用SQLAllocHandle分配的,该函数接受要分配的句柄类型和指向句柄的指针作为输入,然后驱动程序创建一个指定类型的新句柄并将其返回给应用程序。

DuckDB ODBC 驱动程序具有以下句柄类型。

环境

处理名称 Environment
类型名称 SQL_HANDLE_ENV
描述 管理ODBC操作的环境设置,并提供访问数据的全局上下文。
用例 初始化ODBC,管理驱动程序行为,资源分配
附加信息 必须在应用程序启动时分配一次,并在结束时释放。

Connection

处理名称 Connection
类型名称 SQL_HANDLE_DBC
描述 表示与数据源的连接。用于建立、管理和终止连接。定义了驱动程序和要在驱动程序中使用的数据源。
用例 建立与数据库的连接,管理连接状态
附加信息 可以根据需要创建多个连接句柄,允许同时连接到多个数据源。注意:分配连接句柄并不会建立连接,但必须先分配,然后在连接建立后使用。

Statement

句柄名称 Statement
类型名称 SQL_HANDLE_STMT
描述 处理SQL语句的执行以及返回的结果集。
用例 执行SQL查询,获取结果集,管理语句选项。
附加信息 为了促进并发查询的执行,每个连接可以分配多个句柄。

描述符

句柄名称 Descriptor
类型名称 SQL_HANDLE_DESC
描述 描述数据结构或参数的属性,并允许应用程序指定要绑定/检索的数据结构。
用例 描述表结构、结果集,将列绑定到应用程序缓冲区
附加信息 用于需要显式定义数据结构的情况,例如在参数绑定或结果集获取期间。它们在分配语句时自动分配,但也可以显式分配。

Connecting

第一步是连接到数据源,以便应用程序可以执行数据库操作。首先,应用程序必须分配一个环境句柄,然后是一个连接句柄。连接句柄随后用于连接到数据源。有两个函数可以用于连接到数据源,SQLDriverConnectSQLConnect。前者用于使用连接字符串连接到数据源,而后者用于使用DSN连接到数据源。

Connection String

一个连接字符串是一个包含连接到数据源所需信息的字符串。它被格式化为一个由分号分隔的键值对列表,然而DuckDB目前只使用DSN并忽略其余参数。

DSN

DSN(数据源名称)是一个标识数据库的字符串。它可以是文件路径、URL或数据库名称。例如:C:\Users\me\duckdb.dbDuckDB 都是有效的DSN。有关DSN的更多信息可以在SQL Server文档的“选择数据源或驱动程序”页面上找到。

错误处理与诊断

ODBC中的所有函数都返回一个代码,表示函数的成功或失败。这使得错误处理变得容易,因为应用程序可以简单地检查每个函数调用的返回代码以确定是否成功。当不成功时,应用程序可以使用SQLGetDiagRec函数来检索错误信息。下表定义了返回代码

返回代码 描述
SQL_SUCCESS 函数成功完成。
SQL_SUCCESS_WITH_INFO 函数成功完成,但有额外的信息可用,包括一个警告
SQL_ERROR 函数执行失败。
SQL_INVALID_HANDLE 提供的句柄无效,表明存在编程错误,例如在使用句柄之前未分配句柄,或者句柄类型错误。
SQL_NO_DATA 函数成功完成,但没有更多数据可用
SQL_NEED_DATA 需要更多数据,例如在执行时发送参数数据,或需要额外的连接信息。
SQL_STILL_EXECUTING 一个异步执行的函数仍在执行中。

缓冲区和绑定

缓冲区是用于存储数据的内存块。缓冲区用于存储从数据库检索的数据,或发送数据到数据库。缓冲区由应用程序分配,然后使用SQLBindColSQLBindParameter函数绑定到结果集中的列或查询中的参数。当应用程序从结果集中获取一行或执行查询时,数据存储在缓冲区中。当应用程序向数据库发送查询时,缓冲区中的数据被发送到数据库。

设置应用程序

以下是设置一个使用ODBC连接到数据库、执行查询并在C++中获取结果的应用程序的逐步指南。

要安装驱动程序以及您需要的其他任何内容,请按照这些说明进行操作。

1. 包含SQL头文件

第一步是包含SQL头文件:

#include <sql.h>
#include <sqlext.h>

这些文件包含ODBC函数的定义,以及ODBC使用的数据类型。为了能够使用这些头文件,您必须安装unixodbc包:

在macOS上:

brew install unixodbc

在Ubuntu和Debian上:

sudo apt-get install -y unixodbc-dev

在Fedora、CentOS和Red Hat上:

sudo yum install -y unixODBC-devel

记得在你的CFLAGS中包含头文件的位置。

对于 MAKEFILE:

CFLAGS=-I/usr/local/include
# or
CFLAGS=-/opt/homebrew/Cellar/unixodbc/2.3.11/include

对于 CMAKE:

include_directories(/usr/local/include)
# or
include_directories(/opt/homebrew/Cellar/unixodbc/2.3.11/include)

你还需要在你的CMAKEMAKEFILE中链接库。 对于CMAKE

target_link_libraries(ODBC_application /path/to/duckdb_odbc/libduckdb_odbc.dylib)

对于 MAKEFILE:

LDLIBS=-L/path/to/duckdb_odbc/libduckdb_odbc.dylib

2. 定义ODBC句柄并连接到数据库

2.a. 使用SQLConnect连接

然后设置ODBC句柄,分配它们,并连接到数据库。首先分配环境句柄,然后将环境设置为ODBC版本3,接着分配连接句柄,最后连接到数据库。以下代码片段展示了如何执行此操作:

SQLHANDLE env;
SQLHANDLE dbc;

SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);

SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);

SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);

std::string dsn = "DSN=duckdbmemory";
SQLConnect(dbc, (SQLCHAR*)dsn.c_str(), SQL_NTS, NULL, 0, NULL, 0);

std::cout << "Connected!" << std::endl;

2.b. 使用SQLDriverConnect连接

或者,您可以使用SQLDriverConnect连接到ODBC驱动程序。 SQLDriverConnect接受一个连接字符串,您可以在其中使用任何可用的DuckDB配置选项来配置数据库。

SQLHANDLE env;
SQLHANDLE dbc;

SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);

SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);

SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);

SQLCHAR str[1024];
SQLSMALLINT strl;
std::string dsn = "DSN=DuckDB;allow_unsigned_extensions=true;access_mode=READ_ONLY"
SQLDriverConnect(dbc, nullptr, (SQLCHAR*)dsn.c_str(), SQL_NTS, str, sizeof(str), &strl, SQL_DRIVER_COMPLETE)

std::cout << "Connected!" << std::endl;

3. 添加查询

现在应用程序已经设置好了,我们可以向其中添加一个查询。首先,我们需要分配一个语句句柄:

SQLHANDLE stmt;
SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);

然后我们可以执行一个查询:

SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM integers", SQL_NTS);

4. 获取结果

现在我们已经执行了一个查询,我们可以获取结果。首先,我们需要将结果集中的列绑定到缓冲区:

SQLLEN int_val;
SQLLEN null_val;
SQLBindCol(stmt, 1, SQL_C_SLONG, &int_val, 0, &null_val);

然后我们可以获取结果:

SQLFetch(stmt);

5. 尽情发挥

现在我们有了结果,我们可以对它们做任何我们想做的事情。例如,我们可以打印它们:

std::cout << "Value: " << int_val << std::endl;

或者执行我们想要的任何其他处理。除了执行更多查询和我们对数据库想要做的任何其他事情,比如插入、更新或删除数据。

6. 释放句柄并断开连接

最后,我们需要释放句柄并断开与数据库的连接。首先,我们需要释放语句句柄:

SQLFreeHandle(SQL_HANDLE_STMT, stmt);

然后我们需要断开与数据库的连接:

SQLDisconnect(dbc);

最后,我们需要释放连接句柄和环境句柄:

SQLFreeHandle(SQL_HANDLE_DBC, dbc);
SQLFreeHandle(SQL_HANDLE_ENV, env);

只有在关闭数据库连接后,才能释放连接和环境句柄。在断开数据库连接之前尝试释放它们将导致错误。

示例应用

以下是一个示例应用程序,其中包括一个cpp文件,该文件连接到数据库,执行查询,获取结果并打印它们。它还从数据库断开连接并释放句柄,并包括一个函数来检查ODBC函数的返回值。它还包括一个CMakeLists.txt文件,可用于构建应用程序。

示例 .cpp 文件

#include <iostream>
#include <sql.h>
#include <sqlext.h>

void check_ret(SQLRETURN ret, std::string msg) {
    if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
        std::cout << ret << ": " << msg << " failed" << std::endl;
        exit(1);
    }
    if (ret == SQL_SUCCESS_WITH_INFO) {
        std::cout << ret << ": " << msg << " succeeded with info" << std::endl;
    }
}

int main() {
    SQLHANDLE env;
    SQLHANDLE dbc;
    SQLRETURN ret;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    check_ret(ret, "SQLAllocHandle(env)");

    ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
    check_ret(ret, "SQLSetEnvAttr");

    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    check_ret(ret, "SQLAllocHandle(dbc)");

    std::string dsn = "DSN=duckdbmemory";
    ret = SQLConnect(dbc, (SQLCHAR*)dsn.c_str(), SQL_NTS, NULL, 0, NULL, 0);
    check_ret(ret, "SQLConnect");

    std::cout << "Connected!" << std::endl;

    SQLHANDLE stmt;
    ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
    check_ret(ret, "SQLAllocHandle(stmt)");

    ret = SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM integers", SQL_NTS);
    check_ret(ret, "SQLExecDirect(SELECT * FROM integers)");

    SQLLEN int_val;
    SQLLEN null_val;
    ret = SQLBindCol(stmt, 1, SQL_C_SLONG, &int_val, 0, &null_val);
    check_ret(ret, "SQLBindCol");

    ret = SQLFetch(stmt);
    check_ret(ret, "SQLFetch");

    std::cout << "Value: " << int_val << std::endl;

    ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt);
    check_ret(ret, "SQLFreeHandle(stmt)");

    ret = SQLDisconnect(dbc);
    check_ret(ret, "SQLDisconnect");

    ret = SQLFreeHandle(SQL_HANDLE_DBC, dbc);
    check_ret(ret, "SQLFreeHandle(dbc)");

    ret = SQLFreeHandle(SQL_HANDLE_ENV, env);
    check_ret(ret, "SQLFreeHandle(env)");
}

示例 CMakelists.txt 文件

cmake_minimum_required(VERSION 3.25)
project(ODBC_Tester_App)

set(CMAKE_CXX_STANDARD 17)
include_directories(/opt/homebrew/Cellar/unixodbc/2.3.11/include)

add_executable(ODBC_Tester_App main.cpp)
target_link_libraries(ODBC_Tester_App /duckdb_odbc/libduckdb_odbc.dylib)