⌘+k ctrl+k
1.3 (稳定版)
搜索快捷键 cmd + k | ctrl + k
ODBC 101: ODBC 的小鸭子主题指南

什么是 ODBC?

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

本页面包含指向官方 Microsoft ODBC 文档的链接,该文档是了解 ODBC 的极佳资源。

一般概念

句柄

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

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

环境

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

连接

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

语句

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

描述符

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

连接

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

连接字符串

连接字符串是包含连接到数据源所需信息的字符串。它以分号分隔的键值对列表格式,但 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;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)